update to 0.2.2

This commit is contained in:
David Nguyen 2025-10-27 09:20:22 -04:00
parent 9f573b9371
commit b285feb132
Signed by: david.nguyen
GPG Key ID: D5FB5A5715829326
125 changed files with 6440 additions and 3281 deletions

16
node_modules/.package-lock.json generated vendored
View File

@ -1,6 +1,6 @@
{ {
"name": "@svrnty/ngx-open-map-wrapper", "name": "@svrnty/ngx-open-map-wrapper",
"version": "0.2.0", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@ -105,9 +105,9 @@
} }
}, },
"node_modules/@maplibre/maplibre-gl-style-spec": { "node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "24.2.0", "version": "24.3.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.2.0.tgz", "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.0.tgz",
"integrity": "sha512-cE80g83fRcBbZbQC70siOUxUK6YJ/5ZkClDZbmm+hzrUbv+J6yntkMmcpdz9DbOrWOM7FHKR5rruc6Q/hWx5cA==", "integrity": "sha512-CTJc/Nvldv+GNQuis29VnyV0TYsFTgQBY3SNagTzZ28oHDsDYJ7LwEmfick4Z30wPwI/4gXe3se8PH2IIfLx2g==",
"license": "ISC", "license": "ISC",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -224,9 +224,9 @@
"peer": true "peer": true
}, },
"node_modules/maplibre-gl": { "node_modules/maplibre-gl": {
"version": "5.7.3", "version": "5.10.0",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.7.3.tgz", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.10.0.tgz",
"integrity": "sha512-T6XsjwcSfOr0vtAt4GTzx4m/vD6nrbR0+61MgMzZ9REQwILSEnhqwNpFuFbDedX6LC3ZWjZWnxN7fN/I66WoDQ==", "integrity": "sha512-eLhlX8Fnpaoo7+uGqggZmXmZld6WrbzOJUPB7G8JB8XpminlTnrQTtXilMHduR8fsNVxrzD8yRRqEoajONc8LQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -237,7 +237,7 @@
"@mapbox/unitbezier": "^0.0.1", "@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^2.0.4", "@mapbox/vector-tile": "^2.0.4",
"@mapbox/whoots-js": "^3.1.0", "@mapbox/whoots-js": "^3.1.0",
"@maplibre/maplibre-gl-style-spec": "^24.1.1", "@maplibre/maplibre-gl-style-spec": "^24.3.0",
"@maplibre/vt-pbf": "^4.0.3", "@maplibre/vt-pbf": "^4.0.3",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/geojson-vt": "3.2.5", "@types/geojson-vt": "3.2.5",

View File

@ -432,6 +432,16 @@
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -428,6 +428,16 @@ var source_vector = {
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -7014,6 +7014,16 @@
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -7010,6 +7010,16 @@ var source_vector = {
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -153,6 +153,16 @@
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -1026,6 +1026,15 @@ export type VectorSourceSpecification = {
* A setting to determine whether a source's tiles are cached locally. * A setting to determine whether a source's tiles are cached locally.
*/ */
"volatile"?: boolean; "volatile"?: boolean;
/**
* The encoding used by this source. Mapbox Vector Tiles encoding is used by default.
*
* @default
* ```json
* "mvt"
* ```
*/
"encoding"?: "mvt" | "mlt";
}; };
export type RasterSourceSpecification = { export type RasterSourceSpecification = {
/** /**
@ -4181,6 +4190,31 @@ export declare function validateStyleMin(style: StyleSpecification, styleSpec?:
}; };
}; };
}; };
encoding: {
type: string;
values: {
mvt: {
doc: string;
};
mlt: {
doc: string;
};
};
default: string;
doc: string;
"sdk-support": {
mvt: {
android: string;
ios: string;
js: string;
};
mlt: {
android: string;
ios: string;
js: string;
};
};
};
"*": { "*": {
type: string; type: string;
doc: string; doc: string;

View File

@ -147,6 +147,16 @@ var source_vector = {
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }

File diff suppressed because one or more lines are too long

View File

@ -364,6 +364,31 @@
} }
} }
}, },
"encoding": {
"type": "enum",
"values": {
"mvt": {
"doc": "Mapbox Vector Tiles. See http://github.com/mapbox/vector-tile-spec for more info."
},
"mlt": {
"doc": "MapLibre Vector Tiles. See https://github.com/maplibre/maplibre-tile-spec for more info."
}
},
"default": "mvt",
"doc": "The encoding used by this source. Mapbox Vector Tiles encoding is used by default.",
"sdk-support": {
"mvt": {
"android": "supported",
"ios": "supported",
"js": "supported"
},
"mlt": {
"android": "https://github.com/maplibre/maplibre-native/issues/3721",
"ios": "https://github.com/maplibre/maplibre-native/issues/3721",
"js": "https://github.com/maplibre/maplibre-gl-js/issues/6258"
}
}
},
"*": { "*": {
"type": "*", "type": "*",
"doc": "Other keys to configure the data source." "doc": "Other keys to configure the data source."
@ -6362,7 +6387,7 @@
"ios": "2.0.0" "ios": "2.0.0"
}, },
"data-driven styling": { "data-driven styling": {
"js": "https://github.com/maplibre/maplibre-gl-js/issues/1235", "js": "5.8.0",
"ios": "https://github.com/maplibre/maplibre-native/issues/744", "ios": "https://github.com/maplibre/maplibre-native/issues/744",
"android": "https://github.com/maplibre/maplibre-native/issues/744" "android": "https://github.com/maplibre/maplibre-native/issues/744"
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@maplibre/maplibre-gl-style-spec", "name": "@maplibre/maplibre-gl-style-spec",
"description": "a specification for maplibre styles", "description": "a specification for maplibre styles",
"version": "24.2.0", "version": "24.3.0",
"author": "MapLibre", "author": "MapLibre",
"keywords": [ "keywords": [
"mapbox", "mapbox",
@ -64,7 +64,7 @@
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-strip": "^3.0.4",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
@ -72,23 +72,23 @@
"@stylistic/eslint-plugin": "^5.4.0", "@stylistic/eslint-plugin": "^5.4.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/node": "^24.5.2", "@types/node": "^24.7.2",
"@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.42.0", "@typescript-eslint/parser": "^8.46.0",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vitest/eslint-plugin": "^1.3.12", "@vitest/eslint-plugin": "^1.3.17",
"@vitest/ui": "3.2.4", "@vitest/ui": "3.2.4",
"dts-bundle-generator": "^9.5.1", "dts-bundle-generator": "^9.5.1",
"eslint": "^9.36.0", "eslint": "^9.37.0",
"eslint-plugin-jsdoc": "^60.1.0", "eslint-plugin-jsdoc": "^61.1.1",
"glob": "^11.0.3", "glob": "^11.0.3",
"globals": "^16.4.0", "globals": "^16.4.0",
"rollup": "^4.52.0", "rollup": "^4.52.4",
"rollup-plugin-preserve-shebang": "^1.0.1", "rollup-plugin-preserve-shebang": "^1.0.1",
"semver": "^7.7.2", "semver": "^7.7.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vitest": "3.2.4" "vitest": "3.2.4"
} }
} }

View File

@ -364,6 +364,31 @@
} }
} }
}, },
"encoding": {
"type": "enum",
"values": {
"mvt": {
"doc": "Mapbox Vector Tiles. See http://github.com/mapbox/vector-tile-spec for more info."
},
"mlt": {
"doc": "MapLibre Vector Tiles. See https://github.com/maplibre/maplibre-tile-spec for more info."
}
},
"default": "mvt",
"doc": "The encoding used by this source. Mapbox Vector Tiles encoding is used by default.",
"sdk-support": {
"mvt": {
"android": "supported",
"ios": "supported",
"js": "supported"
},
"mlt": {
"android": "https://github.com/maplibre/maplibre-native/issues/3721",
"ios": "https://github.com/maplibre/maplibre-native/issues/3721",
"js": "https://github.com/maplibre/maplibre-gl-js/issues/6258"
}
}
},
"*": { "*": {
"type": "*", "type": "*",
"doc": "Other keys to configure the data source." "doc": "Other keys to configure the data source."
@ -6362,7 +6387,7 @@
"ios": "2.0.0" "ios": "2.0.0"
}, },
"data-driven styling": { "data-driven styling": {
"js": "https://github.com/maplibre/maplibre-gl-js/issues/1235", "js": "5.8.0",
"ios": "https://github.com/maplibre/maplibre-native/issues/744", "ios": "https://github.com/maplibre/maplibre-native/issues/744",
"android": "https://github.com/maplibre/maplibre-native/issues/744" "android": "https://github.com/maplibre/maplibre-native/issues/744"
} }

View File

@ -724,7 +724,16 @@ export type VectorSourceSpecification = {
/** /**
* A setting to determine whether a source's tiles are cached locally. * A setting to determine whether a source's tiles are cached locally.
*/ */
"volatile"?: boolean "volatile"?: boolean,
/**
* The encoding used by this source. Mapbox Vector Tiles encoding is used by default.
*
* @default
* ```json
* "mvt"
* ```
*/
"encoding"?: "mvt" | "mlt"
}; };
export type RasterSourceSpecification = { export type RasterSourceSpecification = {

10
node_modules/maplibre-gl/README.md generated vendored
View File

@ -47,11 +47,11 @@ Full documentation for this library [is available here](https://maplibre.org/map
Check out the features through [examples](https://maplibre.org/maplibre-gl-js/docs/examples/). Check out the features through [examples](https://maplibre.org/maplibre-gl-js/docs/examples/).
| Showcases | | | Showcases | |
| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| ![Display a map](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-a-map.png) | ![Third party vector tile source](https://maplibre.org/maplibre-gl-js/docs/assets/examples/3d-terrain.png) | | ![Display a map](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-a-map.png) | ![Third party vector tile source](https://maplibre.org/maplibre-gl-js/docs/assets/examples/3d-terrain.png) |
| ![Animate a series of images](https://maplibre.org/maplibre-gl-js/docs/assets/examples/animate-images.png) | ![Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/assets/examples/create-a-heatmap-layer.png) | | ![Animate a series of images](https://maplibre.org/maplibre-gl-js/docs/assets/examples/animate-a-series-of-images.png) | ![Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/assets/examples/create-a-heatmap-layer.png) |
| ![3D buildings](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-buildings-in-3d.png) | ![Visualize population density](https://maplibre.org/maplibre-gl-js/docs/assets/examples/visualize-population-density.png) | | ![3D buildings](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-buildings-in-3d.png) | ![Visualize population density](https://maplibre.org/maplibre-gl-js/docs/assets/examples/visualize-population-density.png) |
<br /> <br />

View File

@ -22,6 +22,7 @@ import fillExtrusionAttributes from '../src/data/bucket/fill_extrusion_attribute
import {lineLayoutAttributes} from '../src/data/bucket/line_attributes'; import {lineLayoutAttributes} from '../src/data/bucket/line_attributes';
import {lineLayoutAttributesExt} from '../src/data/bucket/line_attributes_ext'; import {lineLayoutAttributesExt} from '../src/data/bucket/line_attributes_ext';
import {patternAttributes} from '../src/data/bucket/pattern_attributes'; import {patternAttributes} from '../src/data/bucket/pattern_attributes';
import {dashAttributes} from '../src/data/bucket/dash_attributes';
// symbol layer specific arrays // symbol layer specific arrays
import { import {
symbolLayoutAttributes, symbolLayoutAttributes,
@ -146,7 +147,8 @@ const layoutAttributes = {
heatmap: circleAttributes, heatmap: circleAttributes,
line: lineLayoutAttributes, line: lineLayoutAttributes,
lineExt: lineLayoutAttributesExt, lineExt: lineLayoutAttributesExt,
pattern: patternAttributes pattern: patternAttributes,
dash: dashAttributes
}; };
for (const name in layoutAttributes) { for (const name in layoutAttributes) {
createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]); createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
/** /**
* MapLibre GL JS * MapLibre GL JS
* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v5.7.3/LICENSE.txt * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v5.10.0/LICENSE.txt
*/ */
var maplibregl = (function () { var maplibregl = (function () {
'use strict'; 'use strict';
@ -8939,6 +8939,43 @@ function getAABB(points) {
} }
return [tlX, tlY, brX, brY]; return [tlX, tlY, brX, brY];
} }
/**
* For a given set of tile ids, returns the edge tile ids for the bounding box.
*/
function getEdgeTiles(tileIDs) {
if (!tileIDs.length)
return new Set();
// set a common zoom for calculation (highest zoom) to reproject all tiles to this same zoom
const targetZ = Math.max(...tileIDs.map(id => id.canonical.z));
// vars to store the min and max tile x/y coordinates for edge finding
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
// project all tiles to targetZ while maintaining the reference to the original tile
const projected = [];
for (const id of tileIDs) {
const { x, y, z } = id.canonical;
const scale = Math.pow(2, targetZ - z);
const px = x * scale;
const py = y * scale;
projected.push({ id, x: px, y: py });
if (px < minX)
minX = px;
if (px > maxX)
maxX = px;
if (py < minY)
minY = py;
if (py > maxY)
maxY = py;
}
// find edge tiles using the reprojected tile ids
const edgeTiles = new Set();
for (const p of projected) {
if (p.x === minX || p.x === maxX || p.y === minY || p.y === maxY) {
edgeTiles.add(p.id);
}
}
return edgeTiles;
}
/** /**
* Given a value `t` that varies between 0 and 1, return * Given a value `t` that varies between 0 and 1, return
* an interpolation function that eases between 0 and 1 in a pleasing * an interpolation function that eases between 0 and 1 in a pleasing
@ -9929,6 +9966,16 @@ var source_vector = {
type: "boolean", type: "boolean",
"default": false "default": false
}, },
encoding: {
type: "enum",
values: {
mvt: {
},
mlt: {
}
},
"default": "mvt"
},
"*": { "*": {
type: "*" type: "*"
} }
@ -11873,10 +11920,11 @@ var paint_line = {
expression: { expression: {
interpolated: false, interpolated: false,
parameters: [ parameters: [
"zoom" "zoom",
"feature"
] ]
}, },
"property-type": "cross-faded" "property-type": "cross-faded-data-driven"
}, },
"line-pattern": { "line-pattern": {
type: "resolvedImage", type: "resolvedImage",
@ -20987,10 +21035,7 @@ validateStyleMin.paintProperty = wrapCleanErrors(injectValidateSpec(validatePain
validateStyleMin.layoutProperty = wrapCleanErrors(injectValidateSpec(validateLayoutProperty$1)); validateStyleMin.layoutProperty = wrapCleanErrors(injectValidateSpec(validateLayoutProperty$1));
function injectValidateSpec(validator) { function injectValidateSpec(validator) {
return function (options) { return function (options) {
return validator({ return validator(Object.assign({}, options, { validateSpec: validate }));
...options,
validateSpec: validate,
});
}; };
} }
function sortErrors(errors) { function sortErrors(errors) {
@ -23761,8 +23806,8 @@ class StyleLayer extends Evented {
// No-op; can be overridden by derived classes. // No-op; can be overridden by derived classes.
return false; return false;
} }
isHidden(zoom) { isHidden(zoom, roundMinZoom = false) {
if (this.minzoom && zoom < this.minzoom) if (this.minzoom && zoom < (roundMinZoom ? Math.floor(this.minzoom) : this.minzoom))
return true; return true;
if (this.maxzoom && zoom >= this.maxzoom) if (this.maxzoom && zoom >= this.maxzoom)
return true; return true;
@ -24209,6 +24254,37 @@ class StructArrayLayout10ui20 extends StructArray {
} }
StructArrayLayout10ui20.prototype.bytesPerElement = 20; StructArrayLayout10ui20.prototype.bytesPerElement = 20;
register('StructArrayLayout10ui20', StructArrayLayout10ui20); register('StructArrayLayout10ui20', StructArrayLayout10ui20);
/**
* @internal
* Implementation of the StructArray layout:
* [0] - Uint16[8]
*
*/
class StructArrayLayout8ui16 extends StructArray {
_refreshViews() {
this.uint8 = new Uint8Array(this.arrayBuffer);
this.uint16 = new Uint16Array(this.arrayBuffer);
}
emplaceBack(v0, v1, v2, v3, v4, v5, v6, v7) {
const i = this.length;
this.resize(i + 1);
return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7);
}
emplace(i, v0, v1, v2, v3, v4, v5, v6, v7) {
const o2 = i * 8;
this.uint16[o2 + 0] = v0;
this.uint16[o2 + 1] = v1;
this.uint16[o2 + 2] = v2;
this.uint16[o2 + 3] = v3;
this.uint16[o2 + 4] = v4;
this.uint16[o2 + 5] = v5;
this.uint16[o2 + 6] = v6;
this.uint16[o2 + 7] = v7;
return i;
}
}
StructArrayLayout8ui16.prototype.bytesPerElement = 16;
register('StructArrayLayout8ui16', StructArrayLayout8ui16);
/** /**
* @internal * @internal
* Implementation of the StructArray layout: * Implementation of the StructArray layout:
@ -24898,6 +24974,8 @@ class LineExtLayoutArray extends StructArrayLayout2f8 {
} }
class PatternLayoutArray extends StructArrayLayout10ui20 { class PatternLayoutArray extends StructArrayLayout10ui20 {
} }
class DashLayoutArray extends StructArrayLayout8ui16 {
}
class SymbolLayoutArray extends StructArrayLayout4i4ui4i24 { class SymbolLayoutArray extends StructArrayLayout4i4ui4i24 {
} }
class SymbolDynamicLayoutArray extends StructArrayLayout3f12 { class SymbolDynamicLayoutArray extends StructArrayLayout3f12 {
@ -25030,6 +25108,12 @@ const patternAttributes = createLayout([
{ name: 'a_pixel_ratio_to', components: 1, type: 'Uint16' }, { name: 'a_pixel_ratio_to', components: 1, type: 'Uint16' },
]); ]);
const dashAttributes = createLayout([
// [0, y, height, width]
{ name: 'a_dasharray_from', components: 4, type: 'Uint16' },
{ name: 'a_dasharray_to', components: 4, type: 'Uint16' },
]);
var murmurhashJs$1 = {exports: {}}; var murmurhashJs$1 = {exports: {}};
var murmurhash3_gc$1 = {exports: {}}; var murmurhash3_gc$1 = {exports: {}};
@ -25477,16 +25561,36 @@ class CrossFadedConstantBinder {
this.patternFrom = posFrom.tlbr; this.patternFrom = posFrom.tlbr;
this.patternTo = posTo.tlbr; this.patternTo = posTo.tlbr;
} }
setConstantDashPositions(dashTo, dashFrom) {
this.dashTo = [0, dashTo.y, dashTo.height, dashTo.width];
this.dashFrom = [0, dashFrom.y, dashFrom.height, dashFrom.width];
}
setUniform(uniform, globals, currentValue, uniformName) { setUniform(uniform, globals, currentValue, uniformName) {
const pos = uniformName === 'u_pattern_to' ? this.patternTo : let value = null;
uniformName === 'u_pattern_from' ? this.patternFrom : if (uniformName === 'u_pattern_to') {
uniformName === 'u_pixel_ratio_to' ? this.pixelRatioTo : value = this.patternTo;
uniformName === 'u_pixel_ratio_from' ? this.pixelRatioFrom : null; }
if (pos) else if (uniformName === 'u_pattern_from') {
uniform.set(pos); value = this.patternFrom;
}
else if (uniformName === 'u_dasharray_to') {
value = this.dashTo;
}
else if (uniformName === 'u_dasharray_from') {
value = this.dashFrom;
}
else if (uniformName === 'u_pixel_ratio_to') {
value = this.pixelRatioTo;
}
else if (uniformName === 'u_pixel_ratio_from') {
value = this.pixelRatioFrom;
}
if (value !== null) {
uniform.set(value);
}
} }
getBinding(context, location, name) { getBinding(context, location, name) {
return name.substr(0, 9) === 'u_pattern' ? return (name.substr(0, 9) === 'u_pattern' || name.substr(0, 12) === 'u_dasharray_') ?
new Uniform4f(context, location) : new Uniform4f(context, location) :
new Uniform1f(context, location); new Uniform1f(context, location);
} }
@ -25611,7 +25715,7 @@ class CompositeExpressionBinder {
return new Uniform1f(context, location); return new Uniform1f(context, location);
} }
} }
class CrossFadedCompositeBinder { class CrossFadedBinder {
constructor(expression, type, useIntegerZoom, zoom, PaintVertexArray, layerId) { constructor(expression, type, useIntegerZoom, zoom, PaintVertexArray, layerId) {
this.expression = expression; this.expression = expression;
this.type = type; this.type = type;
@ -25625,32 +25729,33 @@ class CrossFadedCompositeBinder {
const start = this.zoomInPaintVertexArray.length; const start = this.zoomInPaintVertexArray.length;
this.zoomInPaintVertexArray.resize(length); this.zoomInPaintVertexArray.resize(length);
this.zoomOutPaintVertexArray.resize(length); this.zoomOutPaintVertexArray.resize(length);
this._setPaintValues(start, length, feature.patterns && feature.patterns[this.layerId], options.imagePositions); this._setPaintValues(start, length, this.getPositionIds(feature), options);
} }
updatePaintArray(start, end, feature, featureState, options) { updatePaintArray(start, end, feature, featureState, options) {
this._setPaintValues(start, end, feature.patterns && feature.patterns[this.layerId], options.imagePositions); this._setPaintValues(start, end, this.getPositionIds(feature), options);
} }
_setPaintValues(start, end, patterns, positions) { _setPaintValues(start, end, positionIds, options) {
if (!positions || !patterns) const positions = this.getPositions(options);
if (!positions || !positionIds)
return; return;
const { min, mid, max } = patterns; const min = positions[positionIds.min];
const imageMin = positions[min]; const mid = positions[positionIds.mid];
const imageMid = positions[mid]; const max = positions[positionIds.max];
const imageMax = positions[max]; if (!min || !mid || !max)
if (!imageMin || !imageMid || !imageMax)
return; return;
// We populate two paint arrays because, for cross-faded properties, we don't know which direction // We populate two paint arrays because, for cross-faded properties, we don't know which direction
// we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass // we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass
// unnecessary vertex data to the shaders, we determine which to upload at draw time. // unnecessary vertex data to the shaders, we determine which to upload at draw time.
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
this.zoomInPaintVertexArray.emplace(i, imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1], imageMin.tl[0], imageMin.tl[1], imageMin.br[0], imageMin.br[1], imageMid.pixelRatio, imageMin.pixelRatio); this.emplace(this.zoomInPaintVertexArray, i, mid, min);
this.zoomOutPaintVertexArray.emplace(i, imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1], imageMax.tl[0], imageMax.tl[1], imageMax.br[0], imageMax.br[1], imageMid.pixelRatio, imageMax.pixelRatio); this.emplace(this.zoomOutPaintVertexArray, i, mid, max);
} }
} }
upload(context) { upload(context) {
if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) { if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) {
this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); const attributes = this.getVertexAttributes();
this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, attributes, this.expression.isStateDependent);
this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, attributes, this.expression.isStateDependent);
} }
} }
destroy() { destroy() {
@ -25660,6 +25765,34 @@ class CrossFadedCompositeBinder {
this.zoomInPaintVertexBuffer.destroy(); this.zoomInPaintVertexBuffer.destroy();
} }
} }
class CrossFadedPatternBinder extends CrossFadedBinder {
getPositions(options) {
return options.imagePositions;
}
getPositionIds(feature) {
return feature.patterns && feature.patterns[this.layerId];
}
getVertexAttributes() {
return patternAttributes.members;
}
emplace(array, index, midPos, minMaxPos) {
array.emplace(index, midPos.tlbr[0], midPos.tlbr[1], midPos.tlbr[2], midPos.tlbr[3], minMaxPos.tlbr[0], minMaxPos.tlbr[1], minMaxPos.tlbr[2], minMaxPos.tlbr[3], midPos.pixelRatio, minMaxPos.pixelRatio);
}
}
class CrossFadedDasharrayBinder extends CrossFadedBinder {
getPositions(options) {
return options.dashPositions;
}
getPositionIds(feature) {
return feature.dashes && feature.dashes[this.layerId];
}
getVertexAttributes() {
return dashAttributes.members;
}
emplace(array, index, midPos, minMaxPos) {
array.emplace(index, 0, midPos.y, midPos.height, midPos.width, 0, minMaxPos.y, minMaxPos.height, minMaxPos.width);
}
}
/** /**
* @internal * @internal
* ProgramConfiguration contains the logic for binding style layer properties and tile * ProgramConfiguration contains the logic for binding style layer properties and tile
@ -25706,7 +25839,9 @@ class ProgramConfiguration {
else if (expression.kind === 'source' || isCrossFaded) { else if (expression.kind === 'source' || isCrossFaded) {
const StructArrayLayout = layoutType(property, type, 'source'); const StructArrayLayout = layoutType(property, type, 'source');
this.binders[property] = isCrossFaded ? this.binders[property] = isCrossFaded ?
new CrossFadedCompositeBinder(expression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) : property === 'line-dasharray' ?
new CrossFadedDasharrayBinder(expression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new CrossFadedPatternBinder(expression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new SourceExpressionBinder(expression, names, type, StructArrayLayout); new SourceExpressionBinder(expression, names, type, StructArrayLayout);
keys.push(`/a_${property}`); keys.push(`/a_${property}`);
} }
@ -25725,7 +25860,7 @@ class ProgramConfiguration {
populatePaintArrays(newLength, feature, options) { populatePaintArrays(newLength, feature, options) {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.populatePaintArray(newLength, feature, options); binder.populatePaintArray(newLength, feature, options);
} }
} }
@ -25736,6 +25871,13 @@ class ProgramConfiguration {
binder.setConstantPatternPositions(posTo, posFrom); binder.setConstantPatternPositions(posTo, posFrom);
} }
} }
setConstantDashPositions(dashTo, dashFrom) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof CrossFadedConstantBinder)
binder.setConstantDashPositions(dashTo, dashFrom);
}
}
updatePaintArrays(featureStates, featureMap, vtLayer, layer, options) { updatePaintArrays(featureStates, featureMap, vtLayer, layer, options) {
let dirty = false; let dirty = false;
for (const id in featureStates) { for (const id in featureStates) {
@ -25745,7 +25887,7 @@ class ProgramConfiguration {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ||
binder instanceof CrossFadedCompositeBinder) && binder.expression.isStateDependent === true) { binder instanceof CrossFadedBinder) && binder.expression.isStateDependent === true) {
//AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255 //AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255
const value = layer.paint.get(property); const value = layer.paint.get(property);
binder.expression = value.value; binder.expression = value.value;
@ -25776,9 +25918,10 @@ class ProgramConfiguration {
result.push(binder.paintVertexAttributes[i].name); result.push(binder.paintVertexAttributes[i].name);
} }
} }
else if (binder instanceof CrossFadedCompositeBinder) { else if (binder instanceof CrossFadedBinder) {
for (let i = 0; i < patternAttributes.members.length; i++) { const attributes = binder.getVertexAttributes();
result.push(patternAttributes.members[i].name); for (const attribute of attributes) {
result.push(attribute.name);
} }
} }
} }
@ -25825,7 +25968,7 @@ class ProgramConfiguration {
this._buffers = []; this._buffers = [];
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (crossfade && binder instanceof CrossFadedCompositeBinder) { if (crossfade && binder instanceof CrossFadedBinder) {
const patternVertexBuffer = crossfade.fromScale === 2 ? binder.zoomInPaintVertexBuffer : binder.zoomOutPaintVertexBuffer; const patternVertexBuffer = crossfade.fromScale === 2 ? binder.zoomInPaintVertexBuffer : binder.zoomOutPaintVertexBuffer;
if (patternVertexBuffer) if (patternVertexBuffer)
this._buffers.push(patternVertexBuffer); this._buffers.push(patternVertexBuffer);
@ -25838,7 +25981,7 @@ class ProgramConfiguration {
upload(context) { upload(context) {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.upload(context); binder.upload(context);
} }
this.updatePaintBuffers(); this.updatePaintBuffers();
@ -25846,7 +25989,7 @@ class ProgramConfiguration {
destroy() { destroy() {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.destroy(); binder.destroy();
} }
} }
@ -25906,6 +26049,7 @@ function paintAttributeNames(property, type) {
'text-halo-width': ['halo_width'], 'text-halo-width': ['halo_width'],
'icon-halo-width': ['halo_width'], 'icon-halo-width': ['halo_width'],
'line-gap-width': ['gapwidth'], 'line-gap-width': ['gapwidth'],
'line-dasharray': ['dasharray_to', 'dasharray_from'],
'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
@ -25925,7 +26069,11 @@ function getLayoutException(property) {
'fill-extrusion-pattern': { 'fill-extrusion-pattern': {
'source': PatternLayoutArray, 'source': PatternLayoutArray,
'composite': PatternLayoutArray 'composite': PatternLayoutArray
} },
'line-dasharray': {
'source': DashLayoutArray,
'composite': DashLayoutArray
},
}; };
return propertyExceptions[property]; return propertyExceptions[property];
} }
@ -25946,7 +26094,8 @@ function layoutType(property, type, binderType) {
register('ConstantBinder', ConstantBinder); register('ConstantBinder', ConstantBinder);
register('CrossFadedConstantBinder', CrossFadedConstantBinder); register('CrossFadedConstantBinder', CrossFadedConstantBinder);
register('SourceExpressionBinder', SourceExpressionBinder); register('SourceExpressionBinder', SourceExpressionBinder);
register('CrossFadedCompositeBinder', CrossFadedCompositeBinder); register('CrossFadedPatternBinder', CrossFadedPatternBinder);
register('CrossFadedDasharrayBinder', CrossFadedDasharrayBinder);
register('CompositeExpressionBinder', CompositeExpressionBinder); register('CompositeExpressionBinder', CompositeExpressionBinder);
register('ProgramConfiguration', ProgramConfiguration, { omit: ['_buffers'] }); register('ProgramConfiguration', ProgramConfiguration, { omit: ['_buffers'] });
register('ProgramConfigurationSet', ProgramConfigurationSet); register('ProgramConfigurationSet', ProgramConfigurationSet);
@ -26019,7 +26168,7 @@ class CircleBucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.layoutVertexArray = new CircleLayoutArray(); this.layoutVertexArray = new CircleLayoutArray();
this.indexArray = new TriangleIndexArray(); this.indexArray = new TriangleIndexArray();
this.segments = new SegmentVector(); this.segments = new SegmentVector();
@ -28880,7 +29029,7 @@ class FillBucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.patternFeatures = []; this.patternFeatures = [];
this.layoutVertexArray = new FillLayoutArray(); this.layoutVertexArray = new FillLayoutArray();
this.indexArray = new TriangleIndexArray(); this.indexArray = new TriangleIndexArray();
@ -28891,7 +29040,7 @@ class FillBucket {
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
} }
populate(features, options, canonical) { populate(features, options, canonical) {
this.hasPattern = hasPattern('fill', this.layers, options); this.hasDependencies = hasPattern('fill', this.layers, options);
const fillSortKey = this.layers[0].layout.get('fill-sort-key'); const fillSortKey = this.layers[0].layout.get('fill-sort-key');
const sortFeaturesByKey = !fillSortKey.isConstant(); const sortFeaturesByKey = !fillSortKey.isConstant();
const bucketFeatures = []; const bucketFeatures = [];
@ -28920,7 +29069,7 @@ class FillBucket {
} }
for (const bucketFeature of bucketFeatures) { for (const bucketFeature of bucketFeatures) {
const { geometry, index, sourceLayerIndex } = bucketFeature; const { geometry, index, sourceLayerIndex } = bucketFeature;
if (this.hasPattern) { if (this.hasDependencies) {
const patternFeature = addPatternDependencies('fill', this.layers, bucketFeature, { zoom: this.zoom }, options); const patternFeature = addPatternDependencies('fill', this.layers, bucketFeature, { zoom: this.zoom }, options);
// pattern features are added only once the pattern is loaded into the image atlas // pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures // so are stored during populate until later updated with positions by tile worker in addFeatures
@ -29441,7 +29590,7 @@ class FillExtrusionBucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.layoutVertexArray = new FillExtrusionLayoutArray(); this.layoutVertexArray = new FillExtrusionLayoutArray();
this.centroidVertexArray = new PosArray(); this.centroidVertexArray = new PosArray();
this.indexArray = new TriangleIndexArray(); this.indexArray = new TriangleIndexArray();
@ -29451,7 +29600,7 @@ class FillExtrusionBucket {
} }
populate(features, options, canonical) { populate(features, options, canonical) {
this.features = []; this.features = [];
this.hasPattern = hasPattern('fill-extrusion', this.layers, options); this.hasDependencies = hasPattern('fill-extrusion', this.layers, options);
for (const { feature, id, index, sourceLayerIndex } of features) { for (const { feature, id, index, sourceLayerIndex } of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry; const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry); const evaluationFeature = toEvaluationFeature(feature, needGeometry);
@ -29466,7 +29615,7 @@ class FillExtrusionBucket {
type: feature.type, type: feature.type,
patterns: {} patterns: {}
}; };
if (this.hasPattern) { if (this.hasDependencies) {
this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, { zoom: this.zoom }, options)); this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, { zoom: this.zoom }, options));
} }
else { else {
@ -29863,7 +30012,7 @@ class LineBucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.patternFeatures = []; this.patternFeatures = [];
this.lineClipsArray = []; this.lineClipsArray = [];
this.gradients = {}; this.gradients = {};
@ -29879,7 +30028,7 @@ class LineBucket {
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
} }
populate(features, options, canonical) { populate(features, options, canonical) {
this.hasPattern = hasPattern('line', this.layers, options); this.hasDependencies = hasPattern('line', this.layers, options) || this.hasLineDasharray(this.layers);
const lineSortKey = this.layers[0].layout.get('line-sort-key'); const lineSortKey = this.layers[0].layout.get('line-sort-key');
const sortFeaturesByKey = !lineSortKey.isConstant(); const sortFeaturesByKey = !lineSortKey.isConstant();
const bucketFeatures = []; const bucketFeatures = [];
@ -29899,6 +30048,7 @@ class LineBucket {
index, index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
patterns: {}, patterns: {},
dashes: {},
sortKey sortKey
}; };
bucketFeatures.push(bucketFeature); bucketFeatures.push(bucketFeature);
@ -29910,29 +30060,35 @@ class LineBucket {
} }
for (const bucketFeature of bucketFeatures) { for (const bucketFeature of bucketFeatures) {
const { geometry, index, sourceLayerIndex } = bucketFeature; const { geometry, index, sourceLayerIndex } = bucketFeature;
if (this.hasPattern) { if (this.hasDependencies) {
const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, { zoom: this.zoom }, options); if (hasPattern('line', this.layers, options)) {
addPatternDependencies('line', this.layers, bucketFeature, { zoom: this.zoom }, options);
}
else if (this.hasLineDasharray(this.layers)) {
this.addLineDashDependencies(this.layers, bucketFeature, this.zoom, options);
}
// pattern features are added only once the pattern is loaded into the image atlas // pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures // so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(patternBucketFeature); this.patternFeatures.push(bucketFeature);
} }
else { else {
this.addFeature(bucketFeature, geometry, index, canonical, {}, options.subdivisionGranularity); this.addFeature(bucketFeature, geometry, index, canonical, {}, {}, options.subdivisionGranularity);
} }
const feature = features[index].feature; const feature = features[index].feature;
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
} }
} }
update(states, vtLayer, imagePositions) { update(states, vtLayer, imagePositions, dashPositions) {
if (!this.stateDependentLayers.length) if (!this.stateDependentLayers.length)
return; return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, { this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions imagePositions,
dashPositions
}); });
} }
addFeatures(options, canonical, imagePositions) { addFeatures(options, canonical, imagePositions, dashPositions) {
for (const feature of this.patternFeatures) { for (const feature of this.patternFeatures) {
this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity); this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, dashPositions, options.subdivisionGranularity);
} }
} }
isEmpty() { isEmpty() {
@ -29967,7 +30123,7 @@ class LineBucket {
return { start, end }; return { start, end };
} }
} }
addFeature(feature, geometry, index, canonical, imagePositions, subdivisionGranularity) { addFeature(feature, geometry, index, canonical, imagePositions, dashPositions, subdivisionGranularity) {
const layout = this.layers[0].layout; const layout = this.layers[0].layout;
const join = layout.get('line-join').evaluate(feature, {}); const join = layout.get('line-join').evaluate(feature, {});
const cap = layout.get('line-cap'); const cap = layout.get('line-cap');
@ -29977,7 +30133,7 @@ class LineBucket {
for (const line of geometry) { for (const line of geometry) {
this.addLine(line, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity); this.addLine(line, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity);
} }
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, { imagePositions, canonical }); this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, { imagePositions, dashPositions, canonical });
} }
addLine(vertices, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity) { addLine(vertices, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity) {
this.distance = 0; this.distance = 0;
@ -30268,6 +30424,43 @@ class LineBucket {
this.distance += prev.dist(next); this.distance += prev.dist(next);
this.updateScaledDistance(); this.updateScaledDistance();
} }
hasLineDasharray(layers) {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (dasharrayProperty && !dasharrayProperty.isConstant()) {
return true;
}
}
return false;
}
addLineDashDependencies(layers, bucketFeature, zoom, options) {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (!dasharrayProperty || dasharrayProperty.value.kind === 'constant') {
continue;
}
const round = layer.layout.get('line-cap') === 'round';
const min = {
dasharray: dasharrayProperty.value.evaluate({ zoom: zoom - 1 }, bucketFeature, {}),
round
};
const mid = {
dasharray: dasharrayProperty.value.evaluate({ zoom }, bucketFeature, {}),
round
};
const max = {
dasharray: dasharrayProperty.value.evaluate({ zoom: zoom + 1 }, bucketFeature, {}),
round
};
const minKey = `${min.dasharray.join(',')},${min.round}`;
const midKey = `${mid.dasharray.join(',')},${mid.round}`;
const maxKey = `${max.dasharray.join(',')},${max.round}`;
options.dashDependencies[minKey] = min;
options.dashDependencies[midKey] = mid;
options.dashDependencies[maxKey] = max;
bucketFeature.dashes[layer.id] = { min: minKey, mid: midKey, max: maxKey };
}
}
} }
register('LineBucket', LineBucket, { omit: ['layers', 'patternFeatures'] }); register('LineBucket', LineBucket, { omit: ['layers', 'patternFeatures'] });
@ -30291,7 +30484,7 @@ const getPaint$3 = () => paint$3 = paint$3 || new Properties({
"line-gap-width": new DataDrivenProperty(v8Spec["paint_line"]["line-gap-width"]), "line-gap-width": new DataDrivenProperty(v8Spec["paint_line"]["line-gap-width"]),
"line-offset": new DataDrivenProperty(v8Spec["paint_line"]["line-offset"]), "line-offset": new DataDrivenProperty(v8Spec["paint_line"]["line-offset"]),
"line-blur": new DataDrivenProperty(v8Spec["paint_line"]["line-blur"]), "line-blur": new DataDrivenProperty(v8Spec["paint_line"]["line-blur"]),
"line-dasharray": new CrossFadedProperty(v8Spec["paint_line"]["line-dasharray"]), "line-dasharray": new CrossFadedDataDrivenProperty(v8Spec["paint_line"]["line-dasharray"]),
"line-pattern": new CrossFadedDataDrivenProperty(v8Spec["paint_line"]["line-pattern"]), "line-pattern": new CrossFadedDataDrivenProperty(v8Spec["paint_line"]["line-pattern"]),
"line-gradient": new ColorRampProperty(v8Spec["paint_line"]["line-gradient"]), "line-gradient": new ColorRampProperty(v8Spec["paint_line"]["line-gradient"]),
}); });
@ -32674,13 +32867,13 @@ class SymbolBucket {
constructor(options) { constructor(options) {
this.collisionBoxArray = options.collisionBoxArray; this.collisionBoxArray = options.collisionBoxArray;
this.zoom = options.zoom; this.zoom = options.zoom;
this.overscaling = options.overscaling; this.overscaling = isSafari(globalThis) ? Math.min(options.overscaling, 128) : options.overscaling;
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.pixelRatio = options.pixelRatio; this.pixelRatio = options.pixelRatio;
this.sourceLayerIndex = options.sourceLayerIndex; this.sourceLayerIndex = options.sourceLayerIndex;
this.hasPattern = false; this.hasDependencies = false;
this.hasRTLText = false; this.hasRTLText = false;
this.sortKeyRanges = []; this.sortKeyRanges = [];
this.collisionCircleArray = []; this.collisionCircleArray = [];
@ -34055,7 +34248,8 @@ function getAnchors(line, spacing, maxAngle, shapedText, shapedIcon, glyphSize,
function resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, placeAtMiddle, tileExtent) { function resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, placeAtMiddle, tileExtent) {
const halfLabelLength = labelLength / 2; const halfLabelLength = labelLength / 2;
const lineLength = getLineLength(line); const lineLength = getLineLength(line);
let distance = 0, markedDistance = offset - spacing; let distance = 0;
let markedDistance = offset - spacing;
let anchors = []; let anchors = [];
for (let i = 0; i < line.length - 1; i++) { for (let i = 0; i < line.length - 1; i++) {
const a = line[i], b = line[i + 1]; const a = line[i], b = line[i + 1];
@ -35663,7 +35857,9 @@ class CanonicalTileID {
equals(id) { equals(id) {
return this.z === id.z && this.x === id.x && this.y === id.y; return this.z === id.z && this.x === id.x && this.y === id.y;
} }
// given a list of urls, choose a url template and return a tile URL /**
* given a list of urls, choose a url template and return a tile URL
*/
url(urls, pixelRatio, scheme) { url(urls, pixelRatio, scheme) {
const bbox = getTileBBox(this.x, this.y, this.z); const bbox = getTileBBox(this.x, this.y, this.z);
const quadkey = getQuadkey(this.z, this.x, this.y); const quadkey = getQuadkey(this.z, this.x, this.y);
@ -35724,6 +35920,14 @@ class OverscaledTileID {
equals(id) { equals(id) {
return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical); return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical);
} }
/**
* Returns a new `OverscaledTileID` representing the tile at the target zoom level.
* When targetZ is greater than the current canonical z, the canonical coordinates are unchanged.
* When targetZ is less than the current canonical z, the canonical coordinates are updated.
* @param targetZ - the zoom level to scale to. Must be less than or equal to this.overscaledZ
* @returns a new OverscaledTileID representing the tile at the target zoom level
* @throws if targetZ is greater than this.overscaledZ
*/
scaledTo(targetZ) { scaledTo(targetZ) {
if (targetZ > this.overscaledZ) if (targetZ > this.overscaledZ)
throw new Error(`targetZ > this.overscaledZ; targetZ = ${targetZ}; overscaledZ = ${this.overscaledZ}`); throw new Error(`targetZ > this.overscaledZ; targetZ = ${targetZ}; overscaledZ = ${this.overscaledZ}`);
@ -35735,6 +35939,9 @@ class OverscaledTileID {
return new OverscaledTileID(targetZ, this.wrap, targetZ, this.canonical.x >> zDifference, this.canonical.y >> zDifference); return new OverscaledTileID(targetZ, this.wrap, targetZ, this.canonical.x >> zDifference, this.canonical.y >> zDifference);
} }
} }
isOverscaled() {
return (this.overscaledZ > this.canonical.z);
}
/* /*
* calculateScaledKey is an optimization: * calculateScaledKey is an optimization:
* when withWrap == true, implements the same as this.scaledTo(z).key, * when withWrap == true, implements the same as this.scaledTo(z).key,
@ -35864,6 +36071,7 @@ class WorkerTile {
iconDependencies: {}, iconDependencies: {},
patternDependencies: {}, patternDependencies: {},
glyphDependencies: {}, glyphDependencies: {},
dashDependencies: {},
availableImages, availableImages,
subdivisionGranularity subdivisionGranularity
}; };
@ -35889,11 +36097,7 @@ class WorkerTile {
if (layer.source !== this.source) { if (layer.source !== this.source) {
warnOnce(`layer.source = ${layer.source} does not equal this.source = ${this.source}`); warnOnce(`layer.source = ${layer.source} does not equal this.source = ${this.source}`);
} }
if (layer.minzoom && this.zoom < Math.floor(layer.minzoom)) if (layer.isHidden(this.zoom, true))
continue;
if (layer.maxzoom && this.zoom >= layer.maxzoom)
continue;
if (layer.visibility === 'none')
continue; continue;
recalculateLayers(family, this.zoom, availableImages); recalculateLayers(family, this.zoom, availableImages);
const bucket = buckets[layer.id] = layer.createBucket({ const bucket = buckets[layer.id] = layer.createBucket({
@ -35935,7 +36139,14 @@ class WorkerTile {
this.inFlightDependencies.push(abortController); this.inFlightDependencies.push(abortController);
getPatternsPromise = actor.sendAsync({ type: "GI" /* MessageType.getImages */, data: { icons: patterns, source: this.source, tileID: this.tileID, type: 'patterns' } }, abortController); getPatternsPromise = actor.sendAsync({ type: "GI" /* MessageType.getImages */, data: { icons: patterns, source: this.source, tileID: this.tileID, type: 'patterns' } }, abortController);
} }
const [glyphMap, iconMap, patternMap] = yield Promise.all([getGlyphsPromise, getIconsPromise, getPatternsPromise]); const dashes = options.dashDependencies;
let getDashesPromise = Promise.resolve({});
if (Object.keys(dashes).length) {
const abortController = new AbortController();
this.inFlightDependencies.push(abortController);
getDashesPromise = actor.sendAsync({ type: "GDA" /* MessageType.getDashes */, data: { dashes } }, abortController);
}
const [glyphMap, iconMap, patternMap, dashPositions] = yield Promise.all([getGlyphsPromise, getIconsPromise, getPatternsPromise, getDashesPromise]);
const glyphAtlas = new GlyphAtlas(glyphMap); const glyphAtlas = new GlyphAtlas(glyphMap);
const imageAtlas = new ImageAtlas(iconMap, patternMap); const imageAtlas = new ImageAtlas(iconMap, patternMap);
for (const key in buckets) { for (const key in buckets) {
@ -35953,12 +36164,9 @@ class WorkerTile {
subdivisionGranularity: options.subdivisionGranularity subdivisionGranularity: options.subdivisionGranularity
}); });
} }
else if (bucket.hasPattern && else if (bucket.hasDependencies && (bucket instanceof FillBucket || bucket instanceof FillExtrusionBucket || bucket instanceof LineBucket)) {
(bucket instanceof LineBucket ||
bucket instanceof FillBucket ||
bucket instanceof FillExtrusionBucket)) {
recalculateLayers(bucket.layers, this.zoom, availableImages); recalculateLayers(bucket.layers, this.zoom, availableImages);
bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions); bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions, dashPositions);
} }
} }
this.status = 'done'; this.status = 'done';
@ -35968,6 +36176,7 @@ class WorkerTile {
collisionBoxArray: this.collisionBoxArray, collisionBoxArray: this.collisionBoxArray,
glyphAtlasImage: glyphAtlas.image, glyphAtlasImage: glyphAtlas.image,
imageAtlas, imageAtlas,
dashPositions,
// Only used for benchmarking: // Only used for benchmarking:
glyphMap: this.returnDependencies ? glyphMap : null, glyphMap: this.returnDependencies ? glyphMap : null,
iconMap: this.returnDependencies ? iconMap : null, iconMap: this.returnDependencies ? iconMap : null,
@ -38115,9 +38324,10 @@ function mergeSourceDiffs(existingDiff, newDiff) {
* For a full example, see [mapbox-gl-topojson](https://github.com/developmentseed/mapbox-gl-topojson). * For a full example, see [mapbox-gl-topojson](https://github.com/developmentseed/mapbox-gl-topojson).
*/ */
class GeoJSONWorkerSource extends VectorTileWorkerSource { class GeoJSONWorkerSource extends VectorTileWorkerSource {
constructor() { constructor(actor, layerIndex, availableImages, createGeoJSONIndexFunc = createGeoJSONIndex) {
super(...arguments); super(actor, layerIndex, availableImages);
this._dataUpdateable = new Map(); this._dataUpdateable = new Map();
this._createGeoJSONIndex = createGeoJSONIndexFunc;
} }
loadVectorTile(params, _abortController) { loadVectorTile(params, _abortController) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
@ -38130,8 +38340,8 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
return null; return null;
} }
const geojsonWrapper = new o(geoJSONTile.features, { version: 2, extent: EXTENT$1 }); const geojsonWrapper = new o(geoJSONTile.features, { version: 2, extent: EXTENT$1 });
// Encode the geojson-vt tile into binary vector tile form. This // Encode the geojson-vt tile into binary vector tile form.
// is a convenience that allows `FeatureIndex` to operate the same way // This is a convenience that allows `FeatureIndex` to operate the same way
// across `VectorTileSource` and `GeoJSONSource` data. // across `VectorTileSource` and `GeoJSONSource` data.
let pbf = s(geojsonWrapper); let pbf = s(geojsonWrapper);
if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) { if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) {
@ -38147,7 +38357,10 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
/** /**
* Fetches (if appropriate), parses, and index geojson data into tiles. This * Fetches (if appropriate), parses, and index geojson data into tiles. This
* preparatory method must be called before {@link GeoJSONWorkerSource.loadTile} * preparatory method must be called before {@link GeoJSONWorkerSource.loadTile}
* can correctly serve up tiles. * can correctly serve up tiles. The first call to this method must contain a valid
* {@link params.data}, {@link params.request}, or {@link params.dataDiff}. Subsequent
* calls may omit these parameters to reprocess the existing data (such as to update
* clustering options).
* *
* Defers to {@link GeoJSONWorkerSource.loadAndProcessGeoJSON} for the pre-processing. * Defers to {@link GeoJSONWorkerSource.loadAndProcessGeoJSON} for the pre-processing.
* *
@ -38165,11 +38378,13 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
new RequestPerformance(params.request) : false; new RequestPerformance(params.request) : false;
this._pendingRequest = new AbortController(); this._pendingRequest = new AbortController();
try { try {
this._pendingData = this.loadAndProcessGeoJSON(params, this._pendingRequest); // Load and process data if no data has been loaded previously, or if there is
// a new request, data, or dataDiff to process.
if (!this._pendingData || params.request || params.data || params.dataDiff) {
this._pendingData = this.loadAndProcessGeoJSON(params, this._pendingRequest);
}
const data = yield this._pendingData; const data = yield this._pendingData;
this._geoJSONIndex = params.cluster ? this._geoJSONIndex = this._createGeoJSONIndex(data, params);
new Supercluster(getSuperclusterOptions(params)).load(data.features) :
geojsonvt(data, params.geojsonVtOptions);
this.loaded = {}; this.loaded = {};
const result = { data }; const result = { data };
if (perf) { if (perf) {
@ -38302,6 +38517,10 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
return this._geoJSONIndex.getLeaves(params.clusterId, params.limit, params.offset); return this._geoJSONIndex.getLeaves(params.clusterId, params.limit, params.offset);
} }
} }
function createGeoJSONIndex(data, params) {
return params.cluster ? new Supercluster(getSuperclusterOptions(params)).load(data.features) :
geojsonvt(data, params.geojsonVtOptions);
}
function getSuperclusterOptions({ superclusterOptions, clusterProperties }) { function getSuperclusterOptions({ superclusterOptions, clusterProperties }) {
if (!clusterProperties || !superclusterOptions) if (!clusterProperties || !superclusterOptions)
return superclusterOptions; return superclusterOptions;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
import Point from '@mapbox/point-geometry'; import Point from '@mapbox/point-geometry';
import TinySDF from '@mapbox/tiny-sdf'; import TinySDF from '@mapbox/tiny-sdf';
import { VectorTileFeature, VectorTileLayer } from '@mapbox/vector-tile'; import { VectorTileFeature, VectorTileLayer } from '@mapbox/vector-tile';
import { Color, ColorArray, CompositeExpression, DiffCommand, DiffOperations, Feature, FeatureFilter, FeatureState, FilterSpecification, Formatted, FormattedSection, GeoJSONSourceSpecification, GlobalProperties, ICanonicalTileID, IMercatorCoordinate, ImageSourceSpecification, InterpolationType, LayerSpecification, LightSpecification, NumberArray, Padding, ProjectionSpecification, PromoteIdSpecification, PropertyValueSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, ResolvedImage, SkySpecification, SourceExpression, SourceSpecification, SpriteSpecification, StateSpecification, StylePropertyExpression, StylePropertySpecification, StyleSpecification, TerrainSpecification, TransitionSpecification, VariableAnchorOffsetCollection, VectorSourceSpecification, VideoSourceSpecification } from '@maplibre/maplibre-gl-style-spec'; import { Color, ColorArray, CompositeExpression, DiffCommand, DiffOperations, Feature, Feature as StyleFeature, FeatureFilter, FeatureState, FilterSpecification, Formatted, FormattedSection, GeoJSONSourceSpecification, GlobalProperties, ICanonicalTileID, IMercatorCoordinate, ImageSourceSpecification, InterpolationType, LayerSpecification, LightSpecification, NumberArray, Padding, ProjectionSpecification, PromoteIdSpecification, PropertyValueSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, ResolvedImage, SkySpecification, SourceExpression, SourceSpecification, SpriteSpecification, StateSpecification, StylePropertyExpression, StylePropertySpecification, StyleSpecification, TerrainSpecification, TransitionSpecification, VariableAnchorOffsetCollection, VectorSourceSpecification, VideoSourceSpecification } from '@maplibre/maplibre-gl-style-spec';
import { Options as GeoJSONVTOptions } from 'geojson-vt'; import { Options as GeoJSONVTOptions } from 'geojson-vt';
import { mat2, mat4, vec3, vec4 } from 'gl-matrix'; import { mat2, mat4, vec3, vec4 } from 'gl-matrix';
import KDBush from 'kdbush'; import KDBush from 'kdbush';
@ -388,6 +388,7 @@ declare class PlacedSymbolStruct extends Struct {
set crossTileID(x: number); set crossTileID(x: number);
get associatedIconIndex(): number; get associatedIconIndex(): number;
} }
type PlacedSymbol = PlacedSymbolStruct;
declare class PlacedSymbolArray extends StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48 { declare class PlacedSymbolArray extends StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48 {
/** /**
* Return the PlacedSymbolStruct at the given location in the array. * Return the PlacedSymbolStruct at the given location in the array.
@ -708,6 +709,9 @@ declare class CanonicalTileID implements ICanonicalTileID {
key: string; key: string;
constructor(z: number, x: number, y: number); constructor(z: number, x: number, y: number);
equals(id: ICanonicalTileID): boolean; equals(id: ICanonicalTileID): boolean;
/**
* given a list of urls, choose a url template and return a tile URL
*/
url(urls: Array<string>, pixelRatio: number, scheme?: string | null): string; url(urls: Array<string>, pixelRatio: number, scheme?: string | null): string;
isChildOf(parent: ICanonicalTileID): boolean; isChildOf(parent: ICanonicalTileID): boolean;
getTilePoint(coord: IMercatorCoordinate): Point; getTilePoint(coord: IMercatorCoordinate): Point;
@ -737,7 +741,16 @@ export declare class OverscaledTileID {
constructor(overscaledZ: number, wrap: number, z: number, x: number, y: number); constructor(overscaledZ: number, wrap: number, z: number, x: number, y: number);
clone(): OverscaledTileID; clone(): OverscaledTileID;
equals(id: OverscaledTileID): boolean; equals(id: OverscaledTileID): boolean;
/**
* Returns a new `OverscaledTileID` representing the tile at the target zoom level.
* When targetZ is greater than the current canonical z, the canonical coordinates are unchanged.
* When targetZ is less than the current canonical z, the canonical coordinates are updated.
* @param targetZ - the zoom level to scale to. Must be less than or equal to this.overscaledZ
* @returns a new OverscaledTileID representing the tile at the target zoom level
* @throws if targetZ is greater than this.overscaledZ
*/
scaledTo(targetZ: number): OverscaledTileID; scaledTo(targetZ: number): OverscaledTileID;
isOverscaled(): boolean;
calculateScaledKey(targetZ: number, withWrap: boolean): string; calculateScaledKey(targetZ: number, withWrap: boolean): string;
isChildOf(parent: OverscaledTileID): boolean; isChildOf(parent: OverscaledTileID): boolean;
children(sourceMaxZoom: number): OverscaledTileID[]; children(sourceMaxZoom: number): OverscaledTileID[];
@ -963,13 +976,6 @@ declare class CrossFadedDataDrivenProperty<T> extends DataDrivenProperty<CrossFa
_calculate(min: T, mid: T, max: T, parameters: EvaluationParameters): CrossFaded<T>; _calculate(min: T, mid: T, max: T, parameters: EvaluationParameters): CrossFaded<T>;
interpolate(a: PossiblyEvaluatedPropertyValue<CrossFaded<T>>): PossiblyEvaluatedPropertyValue<CrossFaded<T>>; interpolate(a: PossiblyEvaluatedPropertyValue<CrossFaded<T>>): PossiblyEvaluatedPropertyValue<CrossFaded<T>>;
} }
declare class CrossFadedProperty<T> implements Property<T, CrossFaded<T>> {
specification: StylePropertySpecification;
constructor(specification: StylePropertySpecification);
possiblyEvaluate(value: PropertyValue<T, CrossFaded<T>>, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array<string>): CrossFaded<T>;
_calculate(min: T, mid: T, max: T, parameters: EvaluationParameters): CrossFaded<T>;
interpolate(a?: CrossFaded<T> | null): CrossFaded<T>;
}
declare class ColorRampProperty implements Property<Color, boolean> { declare class ColorRampProperty implements Property<Color, boolean> {
specification: StylePropertySpecification; specification: StylePropertySpecification;
constructor(specification: StylePropertySpecification); constructor(specification: StylePropertySpecification);
@ -2583,38 +2589,24 @@ declare class SourceCache extends Evented {
*/ */
_sourceLoaded: boolean; _sourceLoaded: boolean;
_sourceErrored: boolean; _sourceErrored: boolean;
_tiles: { _tiles: Record<string, Tile>;
[_: string]: Tile;
};
_prevLng: number; _prevLng: number;
_cache: TileCache; _cache: TileCache;
_timers: { _timers: Record<string, ReturnType<typeof setTimeout>>;
[_ in any]: ReturnType<typeof setTimeout>;
};
_cacheTimers: {
[_ in any]: ReturnType<typeof setTimeout>;
};
_maxTileCacheSize: number; _maxTileCacheSize: number;
_maxTileCacheZoomLevels: number; _maxTileCacheZoomLevels: number;
_paused: boolean; _paused: boolean;
_shouldReloadOnResume: boolean; _shouldReloadOnResume: boolean;
_coveredTiles: {
[_: string]: boolean;
};
transform: ITransform; transform: ITransform;
terrain: Terrain; terrain: Terrain;
used: boolean; used: boolean;
usedForTerrain: boolean; usedForTerrain: boolean;
tileSize: number; tileSize: number;
_state: SourceFeatureState; _state: SourceFeatureState;
_loadedParentTiles: {
[_: string]: Tile;
};
_loadedSiblingTiles: {
[_: string]: Tile;
};
_didEmitContent: boolean; _didEmitContent: boolean;
_updated: boolean; _updated: boolean;
_rasterFadeDuration: number;
_maxFadingAncestorLevels: number;
static maxUnderzooming: number; static maxUnderzooming: number;
static maxOverzooming: number; static maxOverzooming: number;
constructor(id: string, options: SourceSpecification | CanvasSourceSpecification, dispatcher: Dispatcher); constructor(id: string, options: SourceSpecification | CanvasSourceSpecification, dispatcher: Dispatcher);
@ -2640,6 +2632,10 @@ declare class SourceCache extends Evented {
getRenderableIds(symbolLayer?: boolean): Array<string>; getRenderableIds(symbolLayer?: boolean): Array<string>;
hasRenderableParent(tileID: OverscaledTileID): boolean; hasRenderableParent(tileID: OverscaledTileID): boolean;
_isIdRenderable(id: string, symbolLayer?: boolean): boolean; _isIdRenderable(id: string, symbolLayer?: boolean): boolean;
/**
* Reload tiles in this source. If source data has changed, reload all tiles using a state of 'expired',
* otherwise reload only non-errored tiles using state of 'reloading'.
*/
reload(sourceDataChanged?: boolean): void; reload(sourceDataChanged?: boolean): void;
_reloadTile(id: string, state: TileState): Promise<void>; _reloadTile(id: string, state: TileState): Promise<void>;
_tileLoaded(tile: Tile, id: string, previousState: TileState): void; _tileLoaded(tile: Tile, id: string, previousState: TileState): void;
@ -2675,26 +2671,22 @@ declare class SourceCache extends Evented {
* - one sheet = ideal tiles at varying overscaledZ * - one sheet = ideal tiles at varying overscaledZ
* - the second sheet = maxCoveringZoom * - the second sheet = maxCoveringZoom
*/ */
_retainLoadedChildren(targetTiles: { _retainLoadedChildren(targetTiles: Record<string, OverscaledTileID>, retain: Record<string, OverscaledTileID>): Record<string, OverscaledTileID>;
[_: string]: OverscaledTileID;
}, retain: {
[_: string]: OverscaledTileID;
}): void;
/** /**
* Return dictionary of qualified loaded descendents for each provided target tile id * Return dictionary of qualified loaded descendents for each provided target tile id
*/ */
_getLoadedDescendents(targetTileIDs: OverscaledTileID[]): { _getLoadedDescendents(targetTileIDs: OverscaledTileID[]): Record<string, Tile[]>;
[_: string]: Tile[];
};
/** /**
* Find a loaded parent of the given tile (up to minCoveringZoom) * Determine if tile ids fully cover the current generation.
* - 1st generation: need 4 children or 1 overscaled child
* - 2nd generation: need 16 children or 1 overscaled child
*/ */
findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): Tile; _areDescendentsComplete(generationIDs: OverscaledTileID[], generationZ: number, ancestorZ: number): boolean;
/** /**
* Find a loaded sibling of the given tile * Get a loaded tile currently in this source.
* - loaded tiles exist in this._tiles - a cached tile is not a loaded tile
*/ */
findLoadedSibling(tileID: OverscaledTileID): Tile; _getLoadedTile(tileID: OverscaledTileID): Tile | null;
_getLoadedTile(tileID: OverscaledTileID): Tile;
/** /**
* Resizes the tile cache based on the current viewport's size * Resizes the tile cache based on the current viewport's size
* or the maxTileCacheSize option passed during map creation * or the maxTileCacheSize option passed during map creation
@ -2705,38 +2697,84 @@ declare class SourceCache extends Evented {
*/ */
updateCacheSize(transform: IReadonlyTransform): void; updateCacheSize(transform: IReadonlyTransform): void;
handleWrapJump(lng: number): void; handleWrapJump(lng: number): void;
_updateCoveredAndRetainedTiles(retain: {
[_: string]: OverscaledTileID;
}, minCoveringZoom: number, idealTileIDs: OverscaledTileID[], terrain?: Terrain): void;
/** /**
* Removes tiles that are outside the viewport and adds new tiles that * Removes tiles that are outside the viewport and adds new tiles that
* are inside the viewport. * are inside the viewport.
*/ */
update(transform: ITransform, terrain?: Terrain): void; update(transform: ITransform, terrain?: Terrain): void;
/**
* Remove raster tiles that are no longer retained
*/
_cleanUpRasterTiles(retain: Record<string, OverscaledTileID>): void;
/**
* Remove vector tiles that are no longer retained and also not needed for symbol fading
*/
_cleanUpVectorTiles(retain: Record<string, OverscaledTileID>): void;
/**
* Add ideal tiles needed for 3D terrain rendering
*/
_addTerrainIdealTiles(idealTileIDs: OverscaledTileID[]): OverscaledTileID[];
releaseSymbolFadeTiles(): void; releaseSymbolFadeTiles(): void;
/** /**
* Set tiles to be retained on update of this source. For ideal tiles that do not have data, retain their loaded * Set tiles to be retained on update of this source. For ideal tiles that do not have data, retain their loaded
* children so they can be displayed as substitutes pending load of each ideal tile (to reduce flickering). * children so they can be displayed as substitutes pending load of each ideal tile (to reduce flickering).
* If no loaded children are available, fallback to seeking loaded parents as an alternative substitute. * If no loaded children are available, fallback to seeking loaded parents as an alternative substitute.
*/ */
_updateRetainedTiles(idealTileIDs: Array<OverscaledTileID>, zoom: number): { _updateRetainedTiles(idealTileIDs: Array<OverscaledTileID>, zoom: number): Record<string, OverscaledTileID>;
[_: string]: OverscaledTileID;
};
_updateLoadedParentTileCache(): void;
/** /**
* Update the cache of loaded sibling tiles * Designate fading bases and parents using a many-to-one relationship where the lower children fade in/out
* with their parents. Raster shaders are not currently designed for a one-to-many fade relationship.
* *
* Sibling tiles are tiles that share the same zoom level and * Tiles that are candidates for fading out must be loaded and rendered tiles, as loading a tile to then
* x/y position but have different wrap values * fade it out would not appear smoothly. The first source of truth for tile fading always starts at the
* Maintaining sibling tile cache allows fading from old to new tiles * ideal tile, which continually changes on map adjustment. The state of the previously rendered ideal
* of the same position and zoom level * tile plane indicates which direction to fade each part of the newer ideal plane (with varying z).
*
* For a pitched map, the back of the map can have decreasing zooms while the front can have increasing zooms.
* Fade logic must therefore adapt dynamically based on the previously rendered ideal tile set.
*/ */
_updateLoadedSiblingTileCache(): void; _updateFadingTiles(idealTileIDs: OverscaledTileID[], retain: Record<string, OverscaledTileID>): void;
/**
* Many-to-one cross-fade. Set 4 ideal tiles as the fading base for a rendered parent tile
* as the fading parent. Here the parent is fading out and the ideal tile is fading in.
*
* Parent tile - fading out -- Fading Parent
*
* Ideal tiles - fading in -- Base Role = Incoming
*
*
*/
_updateFadingAncestor(idealTile: Tile, retain: Record<string, OverscaledTileID>, now: number): boolean;
/**
* Many-to-one cross-fade. Search descendents of ideal tiles as the fading base with the ideal tile
* as the fading parent. Here the children are fading out and the ideal tile is fading in.
*
*
*
* Ideal tiles - fading in -- Fading Parent
*
* Child tiles - fading out -- Base Role = Departing
*
* Try direct children first. If none found, try grandchildren. Stops at the first generation that provides a fader.
*/
_updateFadingDescendents(idealTile: Tile, retain: Record<string, OverscaledTileID>, now: number): boolean;
_updateFadingChildren(idealTile: Tile, childIDs: OverscaledTileID[], retain: Record<string, OverscaledTileID>, now: number): boolean;
/**
* One-to-one self fading for unloaded edge tiles (for panning sideways on map). for loading tiles over gaps it feels
* more natural for them to fade in, however if they are already loaded/cached then there is no need to fade as map will
* look cohesive with no gaps. Note that draw_raster determines fade priority, as many-to-one fade supersedes edge fading.
*/
_updateFadingEdge(idealTile: Tile, edgeTileIDs: Set<OverscaledTileID>, now: number): boolean;
/** /**
* Add a tile, given its coordinate, to the pyramid. * Add a tile, given its coordinate, to the pyramid.
*/ */
_addTile(tileID: OverscaledTileID): Tile; _addTile(tileID: OverscaledTileID): Tile;
/**
* Set a timeout to reload the tile after it expires
*/
_setTileReloadTimer(id: string, tile: Tile): void; _setTileReloadTimer(id: string, tile: Tile): void;
_clearTileReloadTimer(id: string): void;
_resetTileReloadTimers(): void;
/** /**
* Reload any currently renderable tiles that are match one of the incoming `tileId` x/y/z * Reload any currently renderable tiles that are match one of the incoming `tileId` x/y/z
*/ */
@ -2745,7 +2783,11 @@ declare class SourceCache extends Evented {
* Remove a tile, given its id, from the pyramid * Remove a tile, given its id, from the pyramid
*/ */
_removeTile(id: string): void; _removeTile(id: string): void;
/** @internal */ /** @internal
* Handles incoming source data messages (i.e. after the source has been updated via a worker that has fired
* to map.ts data event). For sources with mutable data, the 'content' event fires when the underlying data
* to a source has changed. (i.e. GeoJSONSource.setData and ImageSource.setCoordinates)
*/
private _dataHandler; private _dataHandler;
/** /**
* Remove all tiles from this pyramid * Remove all tiles from this pyramid
@ -2761,6 +2803,7 @@ declare class SourceCache extends Evented {
private transformBbox; private transformBbox;
getVisibleCoordinates(symbolLayer?: boolean): Array<OverscaledTileID>; getVisibleCoordinates(symbolLayer?: boolean): Array<OverscaledTileID>;
hasTransition(): boolean; hasTransition(): boolean;
setRasterFadeDuration(fadeDuration: number): void;
/** /**
* Set the value of a particular state for a feature * Set the value of a particular state for a feature
*/ */
@ -3138,7 +3181,7 @@ declare class SymbolBucket implements Bucket {
iconsNeedLinear: boolean; iconsNeedLinear: boolean;
bucketInstanceId: number; bucketInstanceId: number;
justReloaded: boolean; justReloaded: boolean;
hasPattern: boolean; hasDependencies: boolean;
textSizeData: SizeData; textSizeData: SizeData;
iconSizeData: SizeData; iconSizeData: SizeData;
glyphOffsetArray: GlyphOffsetArray; glyphOffsetArray: GlyphOffsetArray;
@ -3303,7 +3346,10 @@ declare class Mesh {
constructor(vertexBuffer: VertexBuffer, indexBuffer: IndexBuffer, segments: SegmentVector); constructor(vertexBuffer: VertexBuffer, indexBuffer: IndexBuffer, segments: SegmentVector);
destroy(): void; destroy(): void;
} }
type DashEntry = { /**
* A dash entry
*/
export type DashEntry = {
y: number; y: number;
height: number; height: number;
width: number; width: number;
@ -4114,6 +4160,14 @@ interface CoveringTilesDetailsProvider {
*/ */
prepareNextFrame(): void; prepareNextFrame(): void;
} }
/**
* The callback defining how the transform constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds.
* @see [Customize the map transform constrain](https://maplibre.org/maplibre-gl-js/docs/examples/customize-the-map-transform-constrain/)
*/
export type TransformConstrainFunction = (lngLat: LngLat, zoom: number) => {
center: LngLat;
zoom: number;
};
interface ITransformGetters { interface ITransformGetters {
get tileSize(): number; get tileSize(): number;
get tileZoom(): number; get tileZoom(): number;
@ -4181,6 +4235,10 @@ interface ITransformGetters {
get nearZ(): number; get nearZ(): number;
get farZ(): number; get farZ(): number;
get autoCalculateNearFarZ(): boolean; get autoCalculateNearFarZ(): boolean;
/**
* Get center lngLat and zoom to ensure that longitude and latitude bounds are respected and regions beyond the map bounds are not displayed.
*/
get constrain(): TransformConstrainFunction;
} }
interface ITransformMutators { interface ITransformMutators {
clone(): ITransform; clone(): ITransform;
@ -4279,6 +4337,11 @@ interface ITransformMutators {
* @param bounds - A {@link LngLatBounds} object describing the new geographic boundaries of the map. * @param bounds - A {@link LngLatBounds} object describing the new geographic boundaries of the map.
*/ */
setMaxBounds(bounds?: LngLatBounds | null): void; setMaxBounds(bounds?: LngLatBounds | null): void;
/** Sets or clears the callback overriding the transform's default constrain,
* whose responsibility is to respect the longitude and latitude bounds by constraining the viewport's lnglat and zoom.
* @param constrain - A {@link TransformConstrainFunction} callback defining how the viewport should respect the bounds.
*/
setConstrain(constrain?: TransformConstrainFunction | null): void;
/** /**
* @internal * @internal
* Called before rendering to allow the transform implementation * Called before rendering to allow the transform implementation
@ -4408,12 +4471,10 @@ interface IReadonlyTransform extends ITransformGetters {
*/ */
isPointOnMapSurface(p: Point, terrain?: Terrain): boolean; isPointOnMapSurface(p: Point, terrain?: Terrain): boolean;
/** /**
* Get center lngLat and zoom to ensure that longitude and latitude bounds are respected and regions beyond the map bounds are not displayed. * @internal
* The tranform's default callback that ensures that longitude and latitude bounds are respected by the viewport.
*/ */
getConstrained(lngLat: LngLat, zoom: number): { defaultConstrain: TransformConstrainFunction;
center: LngLat;
zoom: number;
};
maxPitchScaleFactor(): number; maxPitchScaleFactor(): number;
/** /**
* The camera looks at the map from a 3D (lng, lat, altitude) location. Let's use `cameraLocation` * The camera looks at the map from a 3D (lng, lat, altitude) location. Let's use `cameraLocation`
@ -4739,6 +4800,7 @@ type WorkerDEMTileParameters = TileParameters & {
export type WorkerTileResult = ExpiryData & { export type WorkerTileResult = ExpiryData & {
buckets: Array<Bucket>; buckets: Array<Bucket>;
imageAtlas: ImageAtlas; imageAtlas: ImageAtlas;
dashPositions: Record<string, DashEntry>;
glyphAtlasImage: AlphaImage; glyphAtlasImage: AlphaImage;
featureIndex: FeatureIndex; featureIndex: FeatureIndex;
collisionBoxArray: CollisionBoxArray; collisionBoxArray: CollisionBoxArray;
@ -4830,7 +4892,7 @@ declare class CollisionIndex {
number, number,
number number
], collisionGroupPredicate?: (key: FeatureKey) => boolean, getElevation?: (x: number, y: number) => number, shift?: Point, simpleProjectionMatrix?: mat4): PlacedBox; ], collisionGroupPredicate?: (key: FeatureKey) => boolean, getElevation?: (x: number, y: number) => number, shift?: Point, simpleProjectionMatrix?: mat4): PlacedBox;
placeCollisionCircles(overlapMode: OverlapMode, symbol: any, lineVertexArray: SymbolLineVertexArray, glyphOffsetArray: GlyphOffsetArray, fontSize: number, unwrappedTileID: UnwrappedTileID, pitchedLabelPlaneMatrix: mat4, showCollisionCircles: boolean, pitchWithMap: boolean, collisionGroupPredicate: (key: FeatureKey) => boolean, circlePixelDiameter: number, textPixelPadding: number, translation: [ placeCollisionCircles(overlapMode: OverlapMode, symbol: PlacedSymbol, lineVertexArray: SymbolLineVertexArray, glyphOffsetArray: GlyphOffsetArray, fontSize: number, unwrappedTileID: UnwrappedTileID, pitchedLabelPlaneMatrix: mat4, showCollisionCircles: boolean, pitchWithMap: boolean, collisionGroupPredicate: (key: FeatureKey) => boolean, circlePixelDiameter: number, textPixelPadding: number, translation: [
number, number,
number number
], getElevation: (x: number, y: number) => number): PlacedCircles; ], getElevation: (x: number, y: number) => number): PlacedCircles;
@ -5071,6 +5133,20 @@ type QueryRenderedFeaturesResultsItem = QueryResultsItem & {
feature: MapGeoJSONFeature; feature: MapGeoJSONFeature;
}; };
type TileState = "loading" | "loaded" | "reloading" | "unloaded" | "errored" | "expired"; type TileState = "loading" | "loaded" | "reloading" | "unloaded" | "errored" | "expired";
type CrossFadeArgs = {
fadingRole: FadingRoles;
fadingDirection: FadingDirections;
fadingParentID?: OverscaledTileID;
fadeEndTime: number;
};
declare enum FadingRoles {
Base = 0,
Parent = 1
}
declare enum FadingDirections {
Departing = 0,
Incoming = 1
}
/** /**
* A tile object is the combination of a Coordinate, which defines * A tile object is the combination of a Coordinate, which defines
* its place, as well as a unique ID and data tracking for its content * its place, as well as a unique ID and data tracking for its content
@ -5087,13 +5163,21 @@ export declare class Tile {
latestRawTileData: ArrayBuffer; latestRawTileData: ArrayBuffer;
imageAtlas: ImageAtlas; imageAtlas: ImageAtlas;
imageAtlasTexture: Texture; imageAtlasTexture: Texture;
dashPositions: {
[_: string]: DashEntry;
};
glyphAtlasImage: AlphaImage; glyphAtlasImage: AlphaImage;
glyphAtlasTexture: Texture; glyphAtlasTexture: Texture;
expirationTime: any; expirationTime: any;
expiredRequestCount: number; expiredRequestCount: number;
state: TileState; state: TileState;
fadingRole: FadingRoles;
fadingDirection: FadingDirections;
fadingParentID: OverscaledTileID;
selfFading: boolean;
timeAdded: number; timeAdded: number;
fadeEndTime: number; fadeEndTime: number;
fadeOpacity: number;
collisionBoxArray: CollisionBoxArray; collisionBoxArray: CollisionBoxArray;
redoWhenDone: boolean; redoWhenDone: boolean;
showCollisionBoxes: boolean; showCollisionBoxes: boolean;
@ -5135,7 +5219,17 @@ export declare class Tile {
* @param size - The tile size * @param size - The tile size
*/ */
constructor(tileID: OverscaledTileID, size: number); constructor(tileID: OverscaledTileID, size: number);
registerFadeDuration(duration: number): void; isRenderable(symbolLayer: boolean): boolean;
/**
* @internal
* Many-to-one crossfade between a base tile and parent/ancestor tile (when zooming)
*/
setCrossFadeLogic({ fadingRole, fadingDirection, fadingParentID, fadeEndTime }: CrossFadeArgs): void;
/**
* Self fading for edge tiles (when panning map)
*/
setSelfFadeLogic(fadeEndTime: number): void;
resetFadeLogic(): void;
wasRequested(): boolean; wasRequested(): boolean;
clearTextures(painter: any): void; clearTextures(painter: any): void;
/** /**
@ -5166,10 +5260,10 @@ export declare class Tile {
setExpiryData(data: ExpiryData): void; setExpiryData(data: ExpiryData): void;
getExpiryTimeout(): number; getExpiryTimeout(): number;
setFeatureState(states: LayerFeatureStates, painter: any): void; setFeatureState(states: LayerFeatureStates, painter: any): void;
holdingForFade(): boolean; holdingForSymbolFade(): boolean;
symbolFadeFinished(): boolean; symbolFadeFinished(): boolean;
clearFadeHold(): void; clearSymbolFadeHold(): void;
setHoldDuration(duration: number): void; setSymbolHoldDuration(duration: number): void;
setDependencies(namespace: string, dependencies: Array<string>): void; setDependencies(namespace: string, dependencies: Array<string>): void;
hasDependency(namespaces: Array<string>, keys: Array<string>): boolean; hasDependency(namespaces: Array<string>, keys: Array<string>): boolean;
} }
@ -5204,7 +5298,7 @@ declare class CircleBucket<Layer extends CircleStyleLayer | HeatmapStyleLayer> i
layoutVertexBuffer: VertexBuffer; layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<Layer>; programConfigurations: ProgramConfigurationSet<Layer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -5283,7 +5377,7 @@ declare class FillBucket implements Bucket {
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
indexArray2: LineIndexArray; indexArray2: LineIndexArray;
indexBuffer2: IndexBuffer; indexBuffer2: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillStyleLayer>; programConfigurations: ProgramConfigurationSet<FillStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
segments2: SegmentVector; segments2: SegmentVector;
@ -5361,7 +5455,7 @@ declare class FillExtrusionBucket implements Bucket {
centroidVertexBuffer: VertexBuffer; centroidVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>; programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -5529,7 +5623,7 @@ declare class LineBucket implements Bucket {
layoutVertexBuffer2: VertexBuffer; layoutVertexBuffer2: VertexBuffer;
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<LineStyleLayer>; programConfigurations: ProgramConfigurationSet<LineStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -5537,9 +5631,13 @@ declare class LineBucket implements Bucket {
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void; populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void;
update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: { update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}, dashPositions: {
[_: string]: DashEntry;
}): void; }): void;
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: { addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}, dashPositions?: {
[_: string]: DashEntry;
}): void; }): void;
isEmpty(): boolean; isEmpty(): boolean;
uploadPending(): boolean; uploadPending(): boolean;
@ -5548,7 +5646,7 @@ declare class LineBucket implements Bucket {
lineFeatureClips(feature: BucketFeature): LineClips | undefined; lineFeatureClips(feature: BucketFeature): LineClips | undefined;
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: { addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}, subdivisionGranularity: SubdivisionGranularitySetting): void; }, dashPositions: Record<string, DashEntry>, subdivisionGranularity: SubdivisionGranularitySetting): void;
addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, canonical: CanonicalTileID | undefined, subdivisionGranularity: SubdivisionGranularitySetting): void; addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, canonical: CanonicalTileID | undefined, subdivisionGranularity: SubdivisionGranularitySetting): void;
/** /**
* Add two vertices to the buffers. * Add two vertices to the buffers.
@ -5564,6 +5662,8 @@ declare class LineBucket implements Bucket {
addHalfVertex({ x, y }: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment): void; addHalfVertex({ x, y }: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment): void;
updateScaledDistance(): void; updateScaledDistance(): void;
updateDistance(prev: Point, next: Point): void; updateDistance(prev: Point, next: Point): void;
private hasLineDasharray;
private addLineDashDependencies;
} }
type LineLayoutProps = { type LineLayoutProps = {
"line-cap": DataConstantProperty<"butt" | "round" | "square">; "line-cap": DataConstantProperty<"butt" | "round" | "square">;
@ -5591,7 +5691,7 @@ type LinePaintProps = {
"line-gap-width": DataDrivenProperty<number>; "line-gap-width": DataDrivenProperty<number>;
"line-offset": DataDrivenProperty<number>; "line-offset": DataDrivenProperty<number>;
"line-blur": DataDrivenProperty<number>; "line-blur": DataDrivenProperty<number>;
"line-dasharray": CrossFadedProperty<Array<number>>; "line-dasharray": CrossFadedDataDrivenProperty<Array<number>>;
"line-pattern": CrossFadedDataDrivenProperty<ResolvedImage>; "line-pattern": CrossFadedDataDrivenProperty<ResolvedImage>;
"line-gradient": ColorRampProperty; "line-gradient": ColorRampProperty;
}; };
@ -5607,7 +5707,7 @@ type LinePaintPropsPossiblyEvaluated = {
"line-gap-width": PossiblyEvaluatedPropertyValue<number>; "line-gap-width": PossiblyEvaluatedPropertyValue<number>;
"line-offset": PossiblyEvaluatedPropertyValue<number>; "line-offset": PossiblyEvaluatedPropertyValue<number>;
"line-blur": PossiblyEvaluatedPropertyValue<number>; "line-blur": PossiblyEvaluatedPropertyValue<number>;
"line-dasharray": CrossFaded<Array<number>>; "line-dasharray": PossiblyEvaluatedPropertyValue<CrossFaded<Array<number>>>;
"line-pattern": PossiblyEvaluatedPropertyValue<CrossFaded<ResolvedImage>>; "line-pattern": PossiblyEvaluatedPropertyValue<CrossFaded<ResolvedImage>>;
"line-gradient": ColorRampProperty; "line-gradient": ColorRampProperty;
}; };
@ -5638,6 +5738,9 @@ type PaintOptions = {
imagePositions: { imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}; };
dashPositions?: {
[_: string]: DashEntry;
};
canonical?: CanonicalTileID; canonical?: CanonicalTileID;
formattedSection?: FormattedSection; formattedSection?: FormattedSection;
globalState?: Record<string, any>; globalState?: Record<string, any>;
@ -5663,6 +5766,7 @@ declare class ProgramConfiguration {
getMaxValue(property: string): number; getMaxValue(property: string): number;
populatePaintArrays(newLength: number, feature: Feature, options: PaintOptions): void; populatePaintArrays(newLength: number, feature: Feature, options: PaintOptions): void;
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition): void; setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition): void;
setConstantDashPositions(dashTo: DashEntry, dashFrom: DashEntry): void;
updatePaintArrays(featureStates: FeatureStates, featureMap: FeaturePositionMap, vtLayer: VectorTileLayer, layer: TypedStyleLayer, options: PaintOptions): boolean; updatePaintArrays(featureStates: FeatureStates, featureMap: FeaturePositionMap, vtLayer: VectorTileLayer, layer: TypedStyleLayer, options: PaintOptions): boolean;
defines(): Array<string>; defines(): Array<string>;
getBinderAttributes(): Array<string>; getBinderAttributes(): Array<string>;
@ -6781,6 +6885,7 @@ export declare class Style extends Evented {
getGlyphs(mapId: string | number, params: GetGlyphsParameters): Promise<GetGlyphsResponse>; getGlyphs(mapId: string | number, params: GetGlyphsParameters): Promise<GetGlyphsResponse>;
getGlyphsUrl(): string; getGlyphsUrl(): string;
setGlyphs(glyphsUrl: string | null, options?: StyleSetterOptions): void; setGlyphs(glyphsUrl: string | null, options?: StyleSetterOptions): void;
getDashes(mapId: string | number, params: GetDashesParameters): Promise<GetDashesResponse>;
/** /**
* Add a sprite. * Add a sprite.
* *
@ -6830,6 +6935,10 @@ type PopulateParameters = {
iconDependencies: {}; iconDependencies: {};
patternDependencies: {}; patternDependencies: {};
glyphDependencies: {}; glyphDependencies: {};
dashDependencies: Record<string, {
round: boolean;
dasharray: Array<number>;
}>;
availableImages: Array<string>; availableImages: Array<string>;
subdivisionGranularity: SubdivisionGranularitySetting; subdivisionGranularity: SubdivisionGranularitySetting;
}; };
@ -6853,6 +6962,7 @@ type BucketFeature = {
"max": string; "max": string;
}; };
}; };
readonly dashes?: NonNullable<StyleFeature["dashes"]>;
sortKey?: number; sortKey?: number;
}; };
/** /**
@ -6879,14 +6989,14 @@ type BucketFeature = {
*/ */
export interface Bucket { export interface Bucket {
layerIds: Array<string>; layerIds: Array<string>;
hasPattern: boolean; hasDependencies: boolean;
readonly layers: Array<any>; readonly layers: Array<any>;
readonly stateDependentLayers: Array<any>; readonly stateDependentLayers: Array<any>;
readonly stateDependentLayerIds: Array<string>; readonly stateDependentLayerIds: Array<string>;
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void; populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void;
update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: { update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}): void; }, dashPositions: Record<string, DashEntry>): void;
isEmpty(): boolean; isEmpty(): boolean;
upload(context: Context): void; upload(context: Context): void;
uploadPending(): boolean; uploadPending(): boolean;
@ -6995,7 +7105,7 @@ export declare abstract class StyleLayer extends Evented {
setPaintProperty(name: string, value: unknown, options?: StyleSetterOptions): boolean; setPaintProperty(name: string, value: unknown, options?: StyleSetterOptions): boolean;
_handleSpecialPaintPropertyUpdate(_: string): void; _handleSpecialPaintPropertyUpdate(_: string): void;
_handleOverridablePaintPropertyUpdate<T, R>(name: string, oldValue: PropertyValue<T, R>, newValue: PropertyValue<T, R>): boolean; _handleOverridablePaintPropertyUpdate<T, R>(name: string, oldValue: PropertyValue<T, R>, newValue: PropertyValue<T, R>): boolean;
isHidden(zoom: number): boolean; isHidden(zoom: number, roundMinZoom?: boolean): boolean;
updateTransitions(parameters: TransitionParameters): void; updateTransitions(parameters: TransitionParameters): void;
hasTransition(): boolean; hasTransition(): boolean;
recalculate(parameters: EvaluationParameters, availableImages: Array<string>): void; recalculate(parameters: EvaluationParameters, availableImages: Array<string>): void;
@ -7130,6 +7240,17 @@ type GetGlyphsResponse = {
type GetImagesResponse = { type GetImagesResponse = {
[_: string]: StyleImage; [_: string]: StyleImage;
}; };
type GetDashesParameters = {
dashes: {
[key: string]: {
dasharray: Array<number>;
round: boolean;
};
};
};
type GetDashesResponse = {
[dashId: string]: DashEntry;
};
/** /**
* All the possible message types that can be sent to and from the worker * All the possible message types that can be sent to and from the worker
*/ */
@ -7143,6 +7264,7 @@ export declare const enum MessageType {
loadTile = "LT", loadTile = "LT",
reloadTile = "RT", reloadTile = "RT",
getGlyphs = "GG", getGlyphs = "GG",
getDashes = "GDA",
getImages = "GI", getImages = "GI",
setImages = "SI", setImages = "SI",
updateGlobalState = "UGS", updateGlobalState = "UGS",
@ -7255,6 +7377,10 @@ export type RequestResponseMessageMap = {
RequestParameters, RequestParameters,
GetResourceResponse<any> GetResourceResponse<any>
]; ];
[MessageType.getDashes]: [
GetDashesParameters,
GetDashesResponse
];
}; };
/** /**
* The message to be sent by the actor * The message to be sent by the actor
@ -8395,9 +8521,10 @@ declare abstract class Camera extends Evented {
* between old and new values. The map will retain its current values for any * between old and new values. The map will retain its current values for any
* details not specified in `options`. * details not specified in `options`.
* *
* Note: The transition will happen instantly if the user has enabled * !!! note "Reduced Motion"
* the `reduced motion` accessibility feature enabled in their operating system, * The transition will happen instantly if the user has enabled
* unless `options` includes `essential: true`. * the `reduced motion` accessibility feature enabled in their operating system,
* unless `options` includes `essential: true`.
* *
* Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`,
* `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`.
@ -8458,9 +8585,10 @@ declare abstract class Camera extends Evented {
* evokes flight. The animation seamlessly incorporates zooming and panning to help * evokes flight. The animation seamlessly incorporates zooming and panning to help
* the user maintain her bearings even after traversing a great distance. * the user maintain her bearings even after traversing a great distance.
* *
* Note: The animation will be skipped, and this will behave equivalently to `jumpTo` * !!! note "Reduced Motion"
* if the user has the `reduced motion` accessibility feature enabled in their operating system, * The animation will be skipped, and this will behave equivalently to `jumpTo`
* unless 'options' includes `essential: true`. * if the user has the `reduced motion` accessibility feature enabled in their operating system,
* unless 'options' includes `essential: true`.
* *
* Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`,
* `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`.
@ -8613,6 +8741,14 @@ type EventsInProgress = {
rotate?: EventInProgress; rotate?: EventInProgress;
drag?: EventInProgress; drag?: EventInProgress;
}; };
type MapControlsScenarioOptions = {
terrain?: Terrain | null;
tr: ITransform;
deltasForHelper: MapControlsDeltas;
preZoomAroundLoc: LngLat;
combinedEventsInProgress: EventsInProgress;
panDelta?: Point;
};
declare class HandlerManager { declare class HandlerManager {
_map: Map$1; _map: Map$1;
_el: HTMLElement; _el: HTMLElement;
@ -8673,6 +8809,7 @@ declare class HandlerManager {
_updateMapTransform(combinedResult: HandlerResult, combinedEventsInProgress: EventsInProgress, deactivatedHandlers: { _updateMapTransform(combinedResult: HandlerResult, combinedEventsInProgress: EventsInProgress, deactivatedHandlers: {
[handlerName: string]: Event$1; [handlerName: string]: Event$1;
}): void; }): void;
_handleMapControls({ terrain, tr, deltasForHelper, preZoomAroundLoc, combinedEventsInProgress, panDelta }: MapControlsScenarioOptions): void;
_fireEvents(newEventsInProgress: EventsInProgress, deactivatedHandlers: { _fireEvents(newEventsInProgress: EventsInProgress, deactivatedHandlers: {
[handlerName: string]: Event$1; [handlerName: string]: Event$1;
}, allowEndAnimation: boolean): void; }, allowEndAnimation: boolean): void;
@ -8766,7 +8903,8 @@ export type MapLayerTouchEvent = MapTouchEvent & {
export type MapSourceDataType = "content" | "metadata" | "visibility" | "idle"; export type MapSourceDataType = "content" | "metadata" | "visibility" | "idle";
/** /**
* `MapLayerEventType` - a mapping between the event name and the event. * `MapLayerEventType` - a mapping between the event name and the event.
* **Note:** These events are compatible with the optional `layerId` parameter. * !!! note
* These events are compatible with the optional `layerId` parameter.
* If `layerId` is included as the second argument in {@link Map.on}, the event listener will fire only when the * If `layerId` is included as the second argument in {@link Map.on}, the event listener will fire only when the
* event action contains a visible portion of the specified layer. * event action contains a visible portion of the specified layer.
* The following example can be used for all the events. * The following example can be used for all the events.
@ -8793,7 +8931,8 @@ export type MapLayerEventType = {
/** /**
* Fired when a pointing device (usually a mouse) is pressed and released twice contains a visible portion of the specified layer. * Fired when a pointing device (usually a mouse) is pressed and released twice contains a visible portion of the specified layer.
* *
* **Note:** Under normal conditions, this event will be preceded by two `click` events. * !!! note
* Under normal conditions, this event will be preceded by two `click` events.
*/ */
dblclick: MapLayerMouseEvent; dblclick: MapLayerMouseEvent;
/** /**
@ -8881,7 +9020,7 @@ export type MapLayerEventType = {
* }); * });
* ``` * ```
*/ */
export type MapEventType = { export interface MapEventType {
/** /**
* Fired when an error occurs. This is GL JS's primary error reporting * Fired when an error occurs. This is GL JS's primary error reporting
* mechanism. We use an event instead of `throw` to better accommodate * mechanism. We use an event instead of `throw` to better accommodate
@ -9025,7 +9164,8 @@ export type MapEventType = {
/** /**
* Fired when a pointing device (usually a mouse) is pressed and released twice at the same point on the map in rapid succession. * Fired when a pointing device (usually a mouse) is pressed and released twice at the same point on the map in rapid succession.
* *
* **Note:** Under normal conditions, this event will be preceded by two `click` events. * !!! note
* Under normal conditions, this event will be preceded by two `click` events.
*/ */
dblclick: MapMouseEvent; dblclick: MapMouseEvent;
/** /**
@ -9158,7 +9298,7 @@ export type MapEventType = {
* Fired when map's projection is modified in other ways than by map being moved. * Fired when map's projection is modified in other ways than by map being moved.
*/ */
projectiontransition: MapProjectionEvent; projectiontransition: MapProjectionEvent;
}; }
/** /**
* The base event for MapLibre * The base event for MapLibre
* *
@ -10246,7 +10386,8 @@ export type MapOptions = {
/** /**
* If set, an {@link AttributionControl} will be added to the map with the provided options. * If set, an {@link AttributionControl} will be added to the map with the provided options.
* To disable the attribution control, pass `false`. * To disable the attribution control, pass `false`.
* Note: showing the logo of MapLibre is not required for using MapLibre. * !!! note
* Showing the logo of MapLibre is not required for using MapLibre.
* @defaultValue compact: true, customAttribution: "MapLibre ...". * @defaultValue compact: true, customAttribution: "MapLibre ...".
*/ */
attributionControl?: false | AttributionControlOptions; attributionControl?: false | AttributionControlOptions;
@ -10346,7 +10487,9 @@ export type MapOptions = {
*/ */
trackResize?: boolean; trackResize?: boolean;
/** /**
* The initial geographical centerpoint of the map. If `center` 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, 0]` Note: MapLibre GL JS uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON. * The initial geographical centerpoint of the map. If `center` 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, 0]`
* !!! note
* MapLibre GL JS uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
* @defaultValue [0, 0] * @defaultValue [0, 0]
*/ */
center?: LngLatLike; center?: LngLatLike;
@ -10408,10 +10551,19 @@ export type MapOptions = {
*/ */
transformCameraUpdate?: CameraUpdateTransformFunction | null; transformCameraUpdate?: CameraUpdateTransformFunction | null;
/** /**
* A patch to apply to the default localization table for UI strings, e.g. control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table). * A callback that overrides how the map constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds.
* @see [Customize the map transform constrain](https://maplibre.org/maplibre-gl-js/docs/examples/customize-the-map-transform-constrain/)
* Expected to return an object containing center and zoom.
* @defaultValue null * @defaultValue null
*/ */
locale?: any; transformConstrain?: TransformConstrainFunction | null;
/**
* A patch to apply to the default localization table for UI strings, e.g. control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table).
* For an example, see https://maplibre.org/maplibre-gl-js/docs/examples/locale-switching/
* Alternatively, search the official plugins page for plugins related to localization.
* @defaultValue null
*/
locale?: Record<string, string>;
/** /**
* Controls the duration of the fade-in/fade-out animation for label collisions after initial map load, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. * Controls the duration of the fade-in/fade-out animation for label collisions after initial map load, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading.
* @defaultValue 300 * @defaultValue 300
@ -10445,7 +10597,8 @@ export type MapOptions = {
* font-family for locally overriding generation of Chinese, Japanese, and Korean characters. * font-family for locally overriding generation of Chinese, Japanese, and Korean characters.
* For these characters, font settings from the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold). * For these characters, font settings from the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold).
* Set to `false`, to enable font settings from the map's style for these glyph ranges. * Set to `false`, to enable font settings from the map's style for these glyph ranges.
* The purpose of this option is to avoid bandwidth-intensive glyph server requests. (See [Use locally generated ideographs](https://maplibre.org/maplibre-gl-js/docs/examples/use-locally-generated-ideographs).) * The purpose of this option is to avoid bandwidth-intensive glyph server requests.
* @see [Use locally generated ideographs](https://maplibre.org/maplibre-gl-js/docs/examples/use-locally-generated-ideographs/)
* @defaultValue 'sans-serif' * @defaultValue 'sans-serif'
*/ */
localIdeographFontFamily?: string | false; localIdeographFontFamily?: string | false;
@ -10581,7 +10734,7 @@ declare class Map$1 extends Camera {
_localIdeographFontFamily: string | false; _localIdeographFontFamily: string | false;
_validateStyle: boolean; _validateStyle: boolean;
_requestManager: RequestManager; _requestManager: RequestManager;
_locale: typeof defaultLocale; _locale: Record<string, string>;
_removed: boolean; _removed: boolean;
_clickTolerance: number; _clickTolerance: number;
_overridePixelRatio: number | null | undefined; _overridePixelRatio: number | null | undefined;
@ -10648,6 +10801,11 @@ declare class Map$1 extends Camera {
* @defaultValue true * @defaultValue true
*/ */
cancelPendingTileRequestsWhileZooming: boolean; cancelPendingTileRequestsWhileZooming: boolean;
/**
* The map transform's callback that overrides the default constrain function.
* @defaultValue null
*/
transformConstrain: TransformConstrainFunction | null;
constructor(options: MapOptions); constructor(options: MapOptions);
/** /**
* @internal * @internal
@ -10939,6 +11097,21 @@ declare class Map$1 extends Camera {
* @see [Render world copies](https://maplibre.org/maplibre-gl-js/docs/examples/render-world-copies/) * @see [Render world copies](https://maplibre.org/maplibre-gl-js/docs/examples/render-world-copies/)
*/ */
setRenderWorldCopies(renderWorldCopies?: boolean | null): Map$1; setRenderWorldCopies(renderWorldCopies?: boolean | null): Map$1;
/** Sets or clears the callback overriding how the map constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds.
*
* @param constrain - A {@link TransformConstrainFunction} callback defining how the viewport should respect the bounds.
*
* `null` clears the callback and reverses the override of the map transform's default constrain function.
* @example
* ```ts
* function customTransformConstrain(lngLat, zoom) {
* return {center: lngLat, zoom: zoom ?? 0};
* };
* map.setTransformConstrain(customTransformConstrain);
* ```
* @see [Customize the map transform constrain](https://maplibre.org/maplibre-gl-js/docs/examples/customize-the-map-transform-constrain/)
*/
setTransformConstrain(constrain?: TransformConstrainFunction | null): Map$1;
/** /**
* Returns a [Point](https://github.com/mapbox/point-geometry) representing pixel coordinates, relative to the map's `container`, * Returns a [Point](https://github.com/mapbox/point-geometry) representing pixel coordinates, relative to the map's `container`,
* that correspond to the specified geographical location. * that correspond to the specified geographical location.
@ -11830,10 +12003,11 @@ declare class Map$1 extends Camera {
* and [maximum zoom level](https://maplibre.org/maplibre-style-spec/layers/#maxzoom)) * and [maximum zoom level](https://maplibre.org/maplibre-style-spec/layers/#maxzoom))
* at which the layer will be rendered. * at which the layer will be rendered.
* *
* Note: For style layers using vector sources, style layers cannot be rendered at zoom levels lower than the * !!! note
* minimum zoom level of the _source layer_ because the data does not exist at those zoom levels. If the minimum * For style layers using vector sources, style layers cannot be rendered at zoom levels lower than the
* zoom level of the source layer is higher than the minimum zoom level defined in the style layer, the style * minimum zoom level of the _source layer_ because the data does not exist at those zoom levels. If the minimum
* layer will not be rendered at all zoom levels in the zoom range. * zoom level of the source layer is higher than the minimum zoom level defined in the style layer, the style
* layer will not be rendered at all zoom levels in the zoom range.
* *
* @param layerId - The ID of the layer to which the zoom extent will be applied. * @param layerId - The ID of the layer to which the zoom extent will be applied.
* @param minzoom - The minimum zoom to set (0-24). * @param minzoom - The minimum zoom to set (0-24).
@ -12044,7 +12218,8 @@ declare class Map$1 extends Camera {
* - For vector or GeoJSON sources, using the [`promoteId`](https://maplibre.org/maplibre-style-spec/sources/#promoteid) option at the time the source is defined. * - For vector or GeoJSON sources, using the [`promoteId`](https://maplibre.org/maplibre-style-spec/sources/#promoteid) option at the time the source is defined.
* - For GeoJSON sources, using the [`generateId`](https://maplibre.org/maplibre-style-spec/sources/#generateid) option to auto-assign an `id` based on the feature's index in the source data. If you change feature data using `map.getSource('some id').setData(..)`, you may need to re-apply state taking into account updated `id` values. * - For GeoJSON sources, using the [`generateId`](https://maplibre.org/maplibre-style-spec/sources/#generateid) option to auto-assign an `id` based on the feature's index in the source data. If you change feature data using `map.getSource('some id').setData(..)`, you may need to re-apply state taking into account updated `id` values.
* *
* _Note: You can use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) to access the values in a feature's state object for the purposes of styling._ * !!! note
* You can use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) to access the values in a feature's state object for the purposes of styling.
* *
* @param feature - Feature identifier. Feature objects returned from * @param feature - Feature identifier. Feature objects returned from
* {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers. * {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers.
@ -12121,7 +12296,8 @@ declare class Map$1 extends Camera {
* A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime.
* Features are identified by their `feature.id` attribute, which can be any number or string. * Features are identified by their `feature.id` attribute, which can be any number or string.
* *
* _Note: To access the values in a feature's state object for the purposes of styling the feature, use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state)._ * !!! note
* To access the values in a feature's state object for the purposes of styling the feature, use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state).
* *
* @param feature - Feature identifier. Feature objects returned from * @param feature - Feature identifier. Feature objects returned from
* {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers. * {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers.
@ -13589,7 +13765,7 @@ export declare class TerrainControl implements IControl {
* .addControl(new GlobeControl()); * .addControl(new GlobeControl());
* ``` * ```
* *
* @see [Display a globe with a fill extrusion layer](https://maplibre.org/maplibre-gl-js/docs/examples/globe-fill-extrusion/) * @see [Display a globe with a fill extrusion layer](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-globe-with-a-fill-extrusion-layer/)
*/ */
export declare class GlobeControl implements IControl { export declare class GlobeControl implements IControl {
_map: Map$1; _map: Map$1;
@ -13602,6 +13778,50 @@ export declare class GlobeControl implements IControl {
_toggleProjection: () => void; _toggleProjection: () => void;
_updateGlobeIcon: () => void; _updateGlobeIcon: () => void;
} }
/**
* Freezes time at a specific timestamp for deterministic rendering.
* Useful for frame-by-frame video capture where each frame needs
* a consistent time value.
*
* @param timestamp - Time in milliseconds to freeze at
* @example
* ```ts
* // Freeze time for video export at 60fps
* setNow(0); // First frame
* // ... render frame ...
* setNow(16.67); // Second frame
* // ... render frame ...
* setNow(33.34); // Third frame
* // ... done ...
* restoreNow(); // Resume normal time
* ```
*/
export declare function setNow(timestamp: number): void;
/**
* Restores normal time flow after freezing with setNow().
* Call this after finishing deterministic rendering operations.
*
* @example
* ```ts
* // After video export, resume normal time
* setNow(0);
* // ... export frames ...
* restoreNow(); // Map animations resume normally
* ```
*/
export declare function restoreNow(): void;
/**
* Returns whether time is currently frozen.
* @returns True if time is frozen via setNow(), false otherwise
* @example
* ```ts
* setNow(1000);
* console.log(isTimeFrozen()); // true
* restoreNow();
* console.log(isTimeFrozen()); // false
* ```
*/
export declare function isTimeFrozen(): boolean;
/** /**
* Initializes resources like WebWorkers that can be shared across maps to lower load * Initializes resources like WebWorkers that can be shared across maps to lower load
* times in some situations. `setWorkerUrl()` and `setWorkerCount()`, if being * times in some situations. `setWorkerUrl()` and `setWorkerCount()`, if being
@ -13741,11 +13961,13 @@ export declare class GeoJSONSource extends Evented implements Source {
_pendingWorkerUpdate: { _pendingWorkerUpdate: {
data?: GeoJSON.GeoJSON | string; data?: GeoJSON.GeoJSON | string;
diff?: GeoJSONSourceDiff; diff?: GeoJSONSourceDiff;
optionsChanged?: boolean;
}; };
_collectResourceTiming: boolean; _collectResourceTiming: boolean;
_removed: boolean; _removed: boolean;
/** @internal */ /** @internal */
constructor(id: string, options: GeoJSONSourceOptions, dispatcher: Dispatcher, eventedParent: Evented); constructor(id: string, options: GeoJSONSourceOptions, dispatcher: Dispatcher, eventedParent: Evented);
private _hasPendingWorkerUpdate;
private _pixelsToTileUnits; private _pixelsToTileUnits;
private _getClusterMaxZoom; private _getClusterMaxZoom;
load(): Promise<void>; load(): Promise<void>;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{ {
"name": "maplibre-gl", "name": "maplibre-gl",
"description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library", "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
"version": "5.7.3", "version": "5.10.0",
"main": "dist/maplibre-gl.js", "main": "dist/maplibre-gl.js",
"style": "dist/maplibre-gl.css", "style": "dist/maplibre-gl.css",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
@ -24,7 +24,7 @@
"@mapbox/unitbezier": "^0.0.1", "@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^2.0.4", "@mapbox/vector-tile": "^2.0.4",
"@mapbox/whoots-js": "^3.1.0", "@mapbox/whoots-js": "^3.1.0",
"@maplibre/maplibre-gl-style-spec": "^24.1.1", "@maplibre/maplibre-gl-style-spec": "^24.3.0",
"@maplibre/vt-pbf": "^4.0.3", "@maplibre/vt-pbf": "^4.0.3",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/geojson-vt": "3.2.5", "@types/geojson-vt": "3.2.5",
@ -43,36 +43,36 @@
"devDependencies": { "devDependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.3.0", "@mapbox/mapbox-gl-rtl-text": "^0.3.0",
"@mapbox/mvt-fixtures": "^3.10.0", "@mapbox/mvt-fixtures": "^3.10.0",
"@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-commonjs": "^28.0.8",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-strip": "^3.0.4",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4", "@rollup/plugin-typescript": "^12.1.4",
"@stylistic/eslint-plugin": "^5.3.1", "@stylistic/eslint-plugin": "^5.5.0",
"@types/benchmark": "^2.1.5", "@types/benchmark": "^2.1.5",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/earcut": "^3.0.0", "@types/earcut": "^3.0.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/gl": "^6.0.5", "@types/gl": "^6.0.5",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^27.0.0",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
"@types/murmurhash-js": "^1.0.6", "@types/murmurhash-js": "^1.0.6",
"@types/nise": "^1.4.5", "@types/nise": "^1.4.5",
"@types/node": "^24.5.2", "@types/node": "^24.9.1",
"@types/offscreencanvas": "^2019.7.3", "@types/offscreencanvas": "^2019.7.3",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@types/react": "^19.1.13", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.2.2",
"@types/request": "^2.48.13", "@types/request": "^2.48.13",
"@types/shuffle-seed": "^1.1.3", "@types/shuffle-seed": "^1.1.3",
"@types/window-or-global": "^1.0.6", "@types/window-or-global": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.43.0", "@typescript-eslint/parser": "^8.43.0",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vitest/eslint-plugin": "^1.3.12", "@vitest/eslint-plugin": "^1.3.23",
"@vitest/ui": "3.2.4", "@vitest/ui": "3.2.4",
"address": "^2.0.3", "address": "^2.0.3",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
@ -82,19 +82,19 @@
"cssnano": "^7.1.1", "cssnano": "^7.1.1",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-queue": "^3.0.7", "d3-queue": "^3.0.7",
"devtools-protocol": "^0.0.1517051", "devtools-protocol": "^0.0.1532728",
"diff": "^8.0.2", "diff": "^8.0.2",
"dts-bundle-generator": "^9.5.1", "dts-bundle-generator": "^9.5.1",
"eslint": "^9.35.0", "eslint": "^9.38.0",
"eslint-plugin-html": "^8.1.3", "eslint-plugin-html": "^8.1.3",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-tsdoc": "0.4.0", "eslint-plugin-tsdoc": "0.4.0",
"expect": "^30.1.2", "expect": "^30.2.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"globals": "^16.4.0", "globals": "^16.4.0",
"is-builtin-module": "^5.0.0", "is-builtin-module": "^5.0.0",
"jsdom": "^26.1.0", "jsdom": "^27.0.1",
"junit-report-builder": "^5.1.1", "junit-report-builder": "^5.1.1",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"mock-geolocation": "^1.0.11", "mock-geolocation": "^1.0.11",
@ -108,25 +108,25 @@
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-inline-svg": "^6.0.0", "postcss-inline-svg": "^6.0.0",
"pretty-bytes": "^7.0.1", "pretty-bytes": "^7.1.0",
"puppeteer": "^24.22.0", "puppeteer": "^24.25.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"rollup": "^4.50.2", "rollup": "^4.52.5",
"rollup-plugin-sourcemaps2": "^0.5.4", "rollup-plugin-sourcemaps2": "^0.5.4",
"rollup-plugin-visualizer": "^6.0.3", "rollup-plugin-visualizer": "^6.0.5",
"rw": "^1.3.3", "rw": "^1.3.3",
"semver": "^7.7.2", "semver": "^7.7.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"shuffle-seed": "^1.1.6", "shuffle-seed": "^1.1.6",
"st": "^3.0.3", "st": "^3.0.3",
"stylelint": "^16.24.0", "stylelint": "^16.25.0",
"stylelint-config-standard": "^39.0.0", "stylelint-config-standard": "^39.0.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typedoc": "^0.28.13", "typedoc": "^0.28.14",
"typedoc-plugin-markdown": "^4.8.1", "typedoc-plugin-markdown": "^4.9.0",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vitest": "3.2.4", "vitest": "3.2.4",
"vitest-webgl-canvas-mock": "^1.1.0" "vitest-webgl-canvas-mock": "^1.1.0"
}, },

View File

@ -250,6 +250,44 @@ class StructArrayLayout10ui20 extends StructArray {
StructArrayLayout10ui20.prototype.bytesPerElement = 20; StructArrayLayout10ui20.prototype.bytesPerElement = 20;
register('StructArrayLayout10ui20', StructArrayLayout10ui20); register('StructArrayLayout10ui20', StructArrayLayout10ui20);
/**
* @internal
* Implementation of the StructArray layout:
* [0] - Uint16[8]
*
*/
class StructArrayLayout8ui16 extends StructArray {
uint8: Uint8Array;
uint16: Uint16Array;
_refreshViews() {
this.uint8 = new Uint8Array(this.arrayBuffer);
this.uint16 = new Uint16Array(this.arrayBuffer);
}
public emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
const i = this.length;
this.resize(i + 1);
return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7);
}
public emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
const o2 = i * 8;
this.uint16[o2 + 0] = v0;
this.uint16[o2 + 1] = v1;
this.uint16[o2 + 2] = v2;
this.uint16[o2 + 3] = v3;
this.uint16[o2 + 4] = v4;
this.uint16[o2 + 5] = v5;
this.uint16[o2 + 6] = v6;
this.uint16[o2 + 7] = v7;
return i;
}
}
StructArrayLayout8ui16.prototype.bytesPerElement = 16;
register('StructArrayLayout8ui16', StructArrayLayout8ui16);
/** /**
* @internal * @internal
* Implementation of the StructArray layout: * Implementation of the StructArray layout:
@ -1093,6 +1131,7 @@ export class HeatmapLayoutArray extends StructArrayLayout2i4 {}
export class LineLayoutArray extends StructArrayLayout2i4ub8 {} export class LineLayoutArray extends StructArrayLayout2i4ub8 {}
export class LineExtLayoutArray extends StructArrayLayout2f8 {} export class LineExtLayoutArray extends StructArrayLayout2f8 {}
export class PatternLayoutArray extends StructArrayLayout10ui20 {} export class PatternLayoutArray extends StructArrayLayout10ui20 {}
export class DashLayoutArray extends StructArrayLayout8ui16 {}
export class SymbolLayoutArray extends StructArrayLayout4i4ui4i24 {} export class SymbolLayoutArray extends StructArrayLayout4i4ui4i24 {}
export class SymbolDynamicLayoutArray extends StructArrayLayout3f12 {} export class SymbolDynamicLayoutArray extends StructArrayLayout3f12 {}
export class SymbolOpacityArray extends StructArrayLayout1ul4 {} export class SymbolOpacityArray extends StructArrayLayout1ul4 {}
@ -1111,6 +1150,7 @@ export {
StructArrayLayout2i4ub8, StructArrayLayout2i4ub8,
StructArrayLayout2f8, StructArrayLayout2f8,
StructArrayLayout10ui20, StructArrayLayout10ui20,
StructArrayLayout8ui16,
StructArrayLayout4i4ui4i24, StructArrayLayout4i4ui4i24,
StructArrayLayout3f12, StructArrayLayout3f12,
StructArrayLayout1ul4, StructArrayLayout1ul4,

View File

@ -9,6 +9,8 @@ import type {CanonicalTileID} from '../source/tile_id';
import type {VectorTileFeature, VectorTileLayer} from '@mapbox/vector-tile'; import type {VectorTileFeature, VectorTileLayer} from '@mapbox/vector-tile';
import type Point from '@mapbox/point-geometry'; import type Point from '@mapbox/point-geometry';
import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
import type {DashEntry} from '../render/line_atlas';
import type {Feature as StyleFeature} from '@maplibre/maplibre-gl-style-spec';
export type BucketParameters<Layer extends TypedStyleLayer> = { export type BucketParameters<Layer extends TypedStyleLayer> = {
index: number; index: number;
@ -26,6 +28,7 @@ export type PopulateParameters = {
iconDependencies: {}; iconDependencies: {};
patternDependencies: {}; patternDependencies: {};
glyphDependencies: {}; glyphDependencies: {};
dashDependencies: Record<string, {round: boolean; dasharray: Array<number>}>;
availableImages: Array<string>; availableImages: Array<string>;
subdivisionGranularity: SubdivisionGranularitySetting; subdivisionGranularity: SubdivisionGranularitySetting;
}; };
@ -51,6 +54,7 @@ export type BucketFeature = {
'max': string; 'max': string;
}; };
}; };
readonly dashes?: NonNullable<StyleFeature['dashes']>;
sortKey?: number; sortKey?: number;
}; };
@ -78,12 +82,12 @@ export type BucketFeature = {
*/ */
export interface Bucket { export interface Bucket {
layerIds: Array<string>; layerIds: Array<string>;
hasPattern: boolean; hasDependencies: boolean;
readonly layers: Array<any>; readonly layers: Array<any>;
readonly stateDependentLayers: Array<any>; readonly stateDependentLayers: Array<any>;
readonly stateDependentLayerIds: Array<string>; readonly stateDependentLayerIds: Array<string>;
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void; populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void;
update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}): void; update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}, dashPositions: Record<string, DashEntry>): void;
isEmpty(): boolean; isEmpty(): boolean;
upload(context: Context): void; upload(context: Context): void;
uploadPending(): boolean; uploadPending(): boolean;

View File

@ -61,7 +61,7 @@ export class CircleBucket<Layer extends CircleStyleLayer | HeatmapStyleLayer> im
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<Layer>; programConfigurations: ProgramConfigurationSet<Layer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -72,7 +72,7 @@ export class CircleBucket<Layer extends CircleStyleLayer | HeatmapStyleLayer> im
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.layoutVertexArray = new CircleLayoutArray(); this.layoutVertexArray = new CircleLayoutArray();
this.indexArray = new TriangleIndexArray(); this.indexArray = new TriangleIndexArray();

View File

@ -51,7 +51,7 @@ export class FillBucket implements Bucket {
indexArray2: LineIndexArray; indexArray2: LineIndexArray;
indexBuffer2: IndexBuffer; indexBuffer2: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillStyleLayer>; programConfigurations: ProgramConfigurationSet<FillStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
segments2: SegmentVector; segments2: SegmentVector;
@ -63,7 +63,7 @@ export class FillBucket implements Bucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.patternFeatures = []; this.patternFeatures = [];
this.layoutVertexArray = new FillLayoutArray(); this.layoutVertexArray = new FillLayoutArray();
@ -76,7 +76,7 @@ export class FillBucket implements Bucket {
} }
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) { populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.hasPattern = hasPattern('fill', this.layers, options); this.hasDependencies = hasPattern('fill', this.layers, options);
const fillSortKey = this.layers[0].layout.get('fill-sort-key'); const fillSortKey = this.layers[0].layout.get('fill-sort-key');
const sortFeaturesByKey = !fillSortKey.isConstant(); const sortFeaturesByKey = !fillSortKey.isConstant();
const bucketFeatures: BucketFeature[] = []; const bucketFeatures: BucketFeature[] = [];
@ -112,7 +112,7 @@ export class FillBucket implements Bucket {
for (const bucketFeature of bucketFeatures) { for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature; const {geometry, index, sourceLayerIndex} = bucketFeature;
if (this.hasPattern) { if (this.hasDependencies) {
const patternFeature = addPatternDependencies('fill', this.layers, bucketFeature, {zoom: this.zoom}, options); const patternFeature = addPatternDependencies('fill', this.layers, bucketFeature, {zoom: this.zoom}, options);
// pattern features are added only once the pattern is loaded into the image atlas // pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures // so are stored during populate until later updated with positions by tile worker in addFeatures

View File

@ -74,7 +74,7 @@ export class FillExtrusionBucket implements Bucket {
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>; programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -86,7 +86,7 @@ export class FillExtrusionBucket implements Bucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.layoutVertexArray = new FillExtrusionLayoutArray(); this.layoutVertexArray = new FillExtrusionLayoutArray();
this.centroidVertexArray = new PosArray(); this.centroidVertexArray = new PosArray();
@ -98,7 +98,7 @@ export class FillExtrusionBucket implements Bucket {
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) { populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.features = []; this.features = [];
this.hasPattern = hasPattern('fill-extrusion', this.layers, options); this.hasDependencies = hasPattern('fill-extrusion', this.layers, options);
for (const {feature, id, index, sourceLayerIndex} of features) { for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry; const needGeometry = this.layers[0]._featureFilter.needGeometry;
@ -116,7 +116,7 @@ export class FillExtrusionBucket implements Bucket {
patterns: {} patterns: {}
}; };
if (this.hasPattern) { if (this.hasDependencies) {
this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, {zoom: this.zoom}, options)); this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, {zoom: this.zoom}, options));
} else { } else {
this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.subdivisionGranularity); this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.subdivisionGranularity);

View File

@ -113,7 +113,7 @@ describe('LineBucket', () => {
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision); ], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
const feature = sourceLayer.feature(0); const feature = sourceLayer.feature(0);
bucket.addFeature(feature as any, feature.loadGeometry(), undefined, undefined, undefined, noSubdivision); bucket.addFeature(feature as any, feature.loadGeometry(), undefined, undefined, undefined, undefined, noSubdivision);
}).not.toThrow(); }).not.toThrow();
}); });
@ -130,10 +130,10 @@ describe('LineBucket', () => {
// first add an initial, small feature to make sure the next one starts at // first add an initial, small feature to make sure the next one starts at
// a non-zero offset // a non-zero offset
bucket.addFeature({} as BucketFeature, [createLine(10)], undefined, undefined, undefined, noSubdivision); bucket.addFeature({} as BucketFeature, [createLine(10)], undefined, undefined, undefined, undefined, noSubdivision);
// add a feature that will break across the group boundary // add a feature that will break across the group boundary
bucket.addFeature({} as BucketFeature, [createLine(128)], undefined, undefined, undefined, noSubdivision); bucket.addFeature({} as BucketFeature, [createLine(128)], undefined, undefined, undefined, undefined, noSubdivision);
// Each polygon must fit entirely within a segment, so we expect the // Each polygon must fit entirely within a segment, so we expect the
// first segment to include the first feature and the first polygon // first segment to include the first feature and the first polygon
@ -173,4 +173,19 @@ describe('LineBucket', () => {
test: {min: 'test-pattern', mid: 'test-pattern', max: 'test-pattern'} test: {min: 'test-pattern', mid: 'test-pattern', max: 'test-pattern'}
}); });
}); });
test('LineBucket line-dasharray with global-state', () => {
const bucket = createLineBucket({id: 'test',
paint: {'line-dasharray': ['coalesce', ['get', 'dasharray'], ['global-state', 'dasharray']]},
globalState: {'dasharray': [3, 3]},
availableImages: []
});
bucket.populate(getFeaturesFromLayer(sourceLayer), createPopulateOptions([]), undefined);
expect(bucket.patternFeatures.length).toBeGreaterThan(0);
expect(bucket.patternFeatures[0].dashes).toEqual({
test: {min: '3,3,false', mid: '3,3,false', max: '3,3,false'}
});
});
}); });

View File

@ -34,6 +34,7 @@ import type {ImagePosition} from '../../render/image_atlas';
import type {VectorTileLayer} from '@mapbox/vector-tile'; import type {VectorTileLayer} from '@mapbox/vector-tile';
import {subdivideVertexLine} from '../../render/subdivision'; import {subdivideVertexLine} from '../../render/subdivision';
import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings'; import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {type DashEntry} from '../../render/line_atlas';
// NOTE ON EXTRUDE SCALE: // NOTE ON EXTRUDE SCALE:
// scale the extrusion vector so that the normal length is this value. // scale the extrusion vector so that the normal length is this value.
@ -115,7 +116,7 @@ export class LineBucket implements Bucket {
indexArray: TriangleIndexArray; indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer; indexBuffer: IndexBuffer;
hasPattern: boolean; hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<LineStyleLayer>; programConfigurations: ProgramConfigurationSet<LineStyleLayer>;
segments: SegmentVector; segments: SegmentVector;
uploaded: boolean; uploaded: boolean;
@ -126,7 +127,7 @@ export class LineBucket implements Bucket {
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.hasPattern = false; this.hasDependencies = false;
this.patternFeatures = []; this.patternFeatures = [];
this.lineClipsArray = []; this.lineClipsArray = [];
this.gradients = {}; this.gradients = {};
@ -145,7 +146,7 @@ export class LineBucket implements Bucket {
} }
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) { populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.hasPattern = hasPattern('line', this.layers, options); this.hasDependencies = hasPattern('line', this.layers, options) || this.hasLineDasharray(this.layers);
const lineSortKey = this.layers[0].layout.get('line-sort-key'); const lineSortKey = this.layers[0].layout.get('line-sort-key');
const sortFeaturesByKey = !lineSortKey.isConstant(); const sortFeaturesByKey = !lineSortKey.isConstant();
const bucketFeatures: BucketFeature[] = []; const bucketFeatures: BucketFeature[] = [];
@ -168,6 +169,7 @@ export class LineBucket implements Bucket {
index, index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
patterns: {}, patterns: {},
dashes: {},
sortKey sortKey
}; };
@ -183,13 +185,18 @@ export class LineBucket implements Bucket {
for (const bucketFeature of bucketFeatures) { for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature; const {geometry, index, sourceLayerIndex} = bucketFeature;
if (this.hasPattern) { if (this.hasDependencies) {
const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, {zoom: this.zoom}, options); if (hasPattern('line', this.layers, options)) {
addPatternDependencies('line', this.layers, bucketFeature, {zoom: this.zoom}, options);
} else if (this.hasLineDasharray(this.layers)) {
this.addLineDashDependencies(this.layers, bucketFeature, this.zoom, options);
}
// pattern features are added only once the pattern is loaded into the image atlas // pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures // so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(patternBucketFeature); this.patternFeatures.push(bucketFeature);
} else { } else {
this.addFeature(bucketFeature, geometry, index, canonical, {}, options.subdivisionGranularity); this.addFeature(bucketFeature, geometry, index, canonical, {}, {}, options.subdivisionGranularity);
} }
const feature = features[index].feature; const feature = features[index].feature;
@ -197,16 +204,17 @@ export class LineBucket implements Bucket {
} }
} }
update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}, dashPositions: {[_: string]: DashEntry}) {
if (!this.stateDependentLayers.length) return; if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, { this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions imagePositions,
dashPositions
}); });
} }
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, dashPositions?: {[_: string]: DashEntry}) {
for (const feature of this.patternFeatures) { for (const feature of this.patternFeatures) {
this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity); this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, dashPositions, options.subdivisionGranularity);
} }
} }
@ -246,7 +254,7 @@ export class LineBucket implements Bucket {
} }
} }
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, subdivisionGranularity: SubdivisionGranularitySetting) { addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, dashPositions: Record<string, DashEntry>, subdivisionGranularity: SubdivisionGranularitySetting) {
const layout = this.layers[0].layout; const layout = this.layers[0].layout;
const join = layout.get('line-join').evaluate(feature, {}); const join = layout.get('line-join').evaluate(feature, {});
const cap = layout.get('line-cap'); const cap = layout.get('line-cap');
@ -258,7 +266,7 @@ export class LineBucket implements Bucket {
this.addLine(line, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity); this.addLine(line, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity);
} }
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions, canonical}); this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions, dashPositions, canonical});
} }
addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, canonical: CanonicalTileID | undefined, subdivisionGranularity: SubdivisionGranularitySetting) { addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, canonical: CanonicalTileID | undefined, subdivisionGranularity: SubdivisionGranularitySetting) {
@ -594,6 +602,51 @@ export class LineBucket implements Bucket {
this.distance += prev.dist(next); this.distance += prev.dist(next);
this.updateScaledDistance(); this.updateScaledDistance();
} }
private hasLineDasharray(layers: Array<LineStyleLayer>): boolean {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (dasharrayProperty && !dasharrayProperty.isConstant()) {
return true;
}
}
return false;
}
private addLineDashDependencies(layers: Array<LineStyleLayer>, bucketFeature: BucketFeature, zoom: number, options: PopulateParameters) {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (!dasharrayProperty || dasharrayProperty.value.kind === 'constant') {
continue;
}
const round = layer.layout.get('line-cap') === 'round';
const min = {
dasharray: dasharrayProperty.value.evaluate({zoom: zoom - 1}, bucketFeature, {}),
round
};
const mid = {
dasharray: dasharrayProperty.value.evaluate({zoom}, bucketFeature, {}),
round
};
const max = {
dasharray: dasharrayProperty.value.evaluate({zoom: zoom + 1}, bucketFeature, {}),
round
};
const minKey = `${min.dasharray.join(',')},${min.round}`;
const midKey = `${mid.dasharray.join(',')},${mid.round}`;
const maxKey = `${max.dasharray.join(',')},${max.round}`;
options.dashDependencies[minKey] = min;
options.dashDependencies[midKey] = mid;
options.dashDependencies[maxKey] = max;
bucketFeature.dashes[layer.id] = {min: minKey, mid: midKey, max: maxKey};
}
}
} }
register('LineBucket', LineBucket, {omit: ['layers', 'patternFeatures']}); register('LineBucket', LineBucket, {omit: ['layers', 'patternFeatures']});

View File

@ -18,7 +18,6 @@ import {SubdivisionGranularitySetting} from '../../render/subdivision_granularit
import {MercatorTransform} from '../../geo/projection/mercator_transform'; import {MercatorTransform} from '../../geo/projection/mercator_transform';
import {createPopulateOptions, loadVectorTile} from '../../../test/unit/lib/tile'; import {createPopulateOptions, loadVectorTile} from '../../../test/unit/lib/tile';
/*eslint new-cap: 0*/
const collisionBoxArray = new CollisionBoxArray(); const collisionBoxArray = new CollisionBoxArray();
const transform = new MercatorTransform(); const transform = new MercatorTransform();
transform.resize(100, 100); transform.resize(100, 100);
@ -33,7 +32,7 @@ function bucketSetup(text = 'abcde') {
return createSymbolBucket('test', 'Test', text, collisionBoxArray); return createSymbolBucket('test', 'Test', text, collisionBoxArray);
} }
function createIndexedFeature(id, index, iconId) { function createIndexedFeature(id: number, index: number, iconId: string): IndexedFeature {
return { return {
feature: { feature: {
extent: 8192, extent: 8192,
@ -42,26 +41,26 @@ function createIndexedFeature(id, index, iconId) {
properties: { properties: {
icon: iconId icon: iconId
}, },
loadGeometry () { loadGeometry() {
return [[{x: 0, y: 0}]]; return [[{x: 0, y: 0}]];
} }
}, },
id, id,
index, index,
sourceLayerIndex: 0 sourceLayerIndex: 0
}; } as any as IndexedFeature;
} }
describe('SymbolBucket', () => { describe('SymbolBucket', () => {
let features; let features: IndexedFeature[];
beforeAll(() => { beforeAll(() => {
// Load point features from fixture tile. // Load point features from fixture tile.
const sourceLayer = loadVectorTile().layers.place_label; const sourceLayer = loadVectorTile().layers.place_label;
features = [{feature: sourceLayer.feature(10)} as IndexedFeature]; features = [{feature: sourceLayer.feature(10)} as IndexedFeature];
}); });
test('SymbolBucket', () => { test('SymbolBucket', () => {
const bucketA = bucketSetup() as any as SymbolBucket; const bucketA = bucketSetup();
const bucketB = bucketSetup() as any as SymbolBucket; const bucketB = bucketSetup();
const options = createPopulateOptions([]); const options = createPopulateOptions([]);
const placement = new Placement(transform, undefined as any, 0, true); const placement = new Placement(transform, undefined as any, 0, true);
const tileID = new OverscaledTileID(0, 0, 0, 0, 0); const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
@ -114,7 +113,7 @@ describe('SymbolBucket', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => { }); const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
SymbolBucket.MAX_GLYPHS = 5; SymbolBucket.MAX_GLYPHS = 5;
const bucket = bucketSetup() as any as SymbolBucket; const bucket = bucketSetup();
const options = {iconDependencies: {}, glyphDependencies: {}} as PopulateParameters; const options = {iconDependencies: {}, glyphDependencies: {}} as PopulateParameters;
bucket.populate(features, options, undefined as any); bucket.populate(features, options, undefined as any);
@ -147,7 +146,7 @@ describe('SymbolBucket', () => {
a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage), a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage),
b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage) b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage)
}; };
const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray) as any as SymbolBucket; const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray);
const options = createPopulateOptions([]); const options = createPopulateOptions([]);
bucket.populate( bucket.populate(
@ -190,7 +189,7 @@ describe('SymbolBucket', () => {
a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage), a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage),
b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage) b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage)
}; };
const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray) as any as SymbolBucket; const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray);
const options = createPopulateOptions([]); const options = createPopulateOptions([]);
bucket.populate( bucket.populate(

View File

@ -37,6 +37,7 @@ import {EvaluationParameters} from '../../style/evaluation_parameters';
import {Formatted, ResolvedImage} from '@maplibre/maplibre-gl-style-spec'; import {Formatted, ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
import {rtlWorkerPlugin} from '../../source/rtl_text_plugin_worker'; import {rtlWorkerPlugin} from '../../source/rtl_text_plugin_worker';
import {getOverlapMode} from '../../style/style_layer/overlap_mode'; import {getOverlapMode} from '../../style/style_layer/overlap_mode';
import {isSafari} from '../../util/util';
import type {CanonicalTileID} from '../../source/tile_id'; import type {CanonicalTileID} from '../../source/tile_id';
import type { import type {
Bucket, Bucket,
@ -323,7 +324,7 @@ export class SymbolBucket implements Bucket {
iconsNeedLinear: boolean; iconsNeedLinear: boolean;
bucketInstanceId: number; bucketInstanceId: number;
justReloaded: boolean; justReloaded: boolean;
hasPattern: boolean; hasDependencies: boolean;
textSizeData: SizeData; textSizeData: SizeData;
iconSizeData: SizeData; iconSizeData: SizeData;
@ -362,13 +363,13 @@ export class SymbolBucket implements Bucket {
constructor(options: BucketParameters<SymbolStyleLayer>) { constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray; this.collisionBoxArray = options.collisionBoxArray;
this.zoom = options.zoom; this.zoom = options.zoom;
this.overscaling = options.overscaling; this.overscaling = isSafari(globalThis) ? Math.min(options.overscaling, 128) : options.overscaling;
this.layers = options.layers; this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id); this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index; this.index = options.index;
this.pixelRatio = options.pixelRatio; this.pixelRatio = options.pixelRatio;
this.sourceLayerIndex = options.sourceLayerIndex; this.sourceLayerIndex = options.sourceLayerIndex;
this.hasPattern = false; this.hasDependencies = false;
this.hasRTLText = false; this.hasRTLText = false;
this.sortKeyRanges = []; this.sortKeyRanges = [];

View File

@ -2,9 +2,10 @@ import {packUint8ToFloat} from '../shaders/encode_attribute';
import {type Color, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec'; import {type Color, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
import {register} from '../util/web_worker_transfer'; import {register} from '../util/web_worker_transfer';
import {PossiblyEvaluatedPropertyValue} from '../style/properties'; import {PossiblyEvaluatedPropertyValue} from '../style/properties';
import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray} from './array_types.g'; import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray, DashLayoutArray} from './array_types.g';
import {clamp} from '../util/util'; import {clamp} from '../util/util';
import {patternAttributes} from './bucket/pattern_attributes'; import {patternAttributes} from './bucket/pattern_attributes';
import {dashAttributes} from './bucket/dash_attributes';
import {EvaluationParameters} from '../style/evaluation_parameters'; import {EvaluationParameters} from '../style/evaluation_parameters';
import {FeaturePositionMap} from './feature_position_map'; import {FeaturePositionMap} from './feature_position_map';
import {type Uniform, Uniform1f, UniformColor, Uniform4f} from '../render/uniform_binding'; import {type Uniform, Uniform1f, UniformColor, Uniform4f} from '../render/uniform_binding';
@ -28,6 +29,7 @@ import type {
} from '@maplibre/maplibre-gl-style-spec'; } from '@maplibre/maplibre-gl-style-spec';
import type {FeatureStates} from '../source/source_state'; import type {FeatureStates} from '../source/source_state';
import type {VectorTileLayer} from '@mapbox/vector-tile'; import type {VectorTileLayer} from '@mapbox/vector-tile';
import type {DashEntry} from '../render/line_atlas';
export type BinderUniform = { export type BinderUniform = {
name: string; name: string;
@ -46,6 +48,9 @@ type PaintOptions = {
imagePositions: { imagePositions: {
[_: string]: ImagePosition; [_: string]: ImagePosition;
}; };
dashPositions?: {
[_: string]: DashEntry;
};
canonical?: CanonicalTileID; canonical?: CanonicalTileID;
formattedSection?: FormattedSection; formattedSection?: FormattedSection;
globalState?: Record<string, any>; globalState?: Record<string, any>;
@ -134,6 +139,8 @@ class CrossFadedConstantBinder implements UniformBinder {
uniformNames: Array<string>; uniformNames: Array<string>;
patternFrom: Array<number>; patternFrom: Array<number>;
patternTo: Array<number>; patternTo: Array<number>;
dashFrom: Array<number>;
dashTo: Array<number>;
pixelRatioFrom: number; pixelRatioFrom: number;
pixelRatioTo: number; pixelRatioTo: number;
@ -152,17 +159,35 @@ class CrossFadedConstantBinder implements UniformBinder {
this.patternTo = posTo.tlbr; this.patternTo = posTo.tlbr;
} }
setConstantDashPositions(dashTo: DashEntry, dashFrom: DashEntry) {
this.dashTo = [0, dashTo.y, dashTo.height, dashTo.width];
this.dashFrom = [0, dashFrom.y, dashFrom.height, dashFrom.width];
}
setUniform(uniform: Uniform<any>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<unknown>, uniformName: string) { setUniform(uniform: Uniform<any>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<unknown>, uniformName: string) {
const pos = let value = null;
uniformName === 'u_pattern_to' ? this.patternTo :
uniformName === 'u_pattern_from' ? this.patternFrom : if (uniformName === 'u_pattern_to') {
uniformName === 'u_pixel_ratio_to' ? this.pixelRatioTo : value = this.patternTo;
uniformName === 'u_pixel_ratio_from' ? this.pixelRatioFrom : null; } else if (uniformName === 'u_pattern_from') {
if (pos) uniform.set(pos); value = this.patternFrom;
} else if (uniformName === 'u_dasharray_to') {
value = this.dashTo;
} else if (uniformName === 'u_dasharray_from') {
value = this.dashFrom;
} else if (uniformName === 'u_pixel_ratio_to') {
value = this.pixelRatioTo;
} else if (uniformName === 'u_pixel_ratio_from') {
value = this.pixelRatioFrom;
}
if (value !== null) {
uniform.set(value);
}
} }
getBinding(context: Context, location: WebGLUniformLocation, name: string): Partial<Uniform<any>> { getBinding(context: Context, location: WebGLUniformLocation, name: string): Partial<Uniform<any>> {
return name.substr(0, 9) === 'u_pattern' ? return (name.substr(0, 9) === 'u_pattern' || name.substr(0, 12) === 'u_dasharray_') ?
new Uniform4f(context, location) : new Uniform4f(context, location) :
new Uniform1f(context, location); new Uniform1f(context, location);
} }
@ -321,7 +346,7 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder {
} }
} }
class CrossFadedCompositeBinder implements AttributeBinder { abstract class CrossFadedBinder<T> implements AttributeBinder {
expression: CompositeExpression; expression: CompositeExpression;
type: string; type: string;
useIntegerZoom: boolean; useIntegerZoom: boolean;
@ -351,45 +376,41 @@ class CrossFadedCompositeBinder implements AttributeBinder {
const start = this.zoomInPaintVertexArray.length; const start = this.zoomInPaintVertexArray.length;
this.zoomInPaintVertexArray.resize(length); this.zoomInPaintVertexArray.resize(length);
this.zoomOutPaintVertexArray.resize(length); this.zoomOutPaintVertexArray.resize(length);
this._setPaintValues(start, length, feature.patterns && feature.patterns[this.layerId], options.imagePositions); this._setPaintValues(start, length, this.getPositionIds(feature), options);
} }
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) { updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
this._setPaintValues(start, end, feature.patterns && feature.patterns[this.layerId], options.imagePositions); this._setPaintValues(start, end, this.getPositionIds(feature), options);
} }
_setPaintValues(start, end, patterns, positions) { abstract getVertexAttributes(): Array<StructArrayMember>;
if (!positions || !patterns) return;
const {min, mid, max} = patterns; protected abstract getPositionIds(feature: Feature): {min: string; mid: string; max: string};
const imageMin = positions[min]; protected abstract getPositions(options: PaintOptions): {[_: string]: T};
const imageMid = positions[mid]; protected abstract emplace(array: StructArray, index: number, midPos: T, minMaxPos: T): void;
const imageMax = positions[max];
if (!imageMin || !imageMid || !imageMax) return; protected _setPaintValues(start: number, end: number, positionIds: {min: string; mid: string; max: string}, options: PaintOptions) {
const positions = this.getPositions(options);
if (!positions || !positionIds) return;
const min = positions[positionIds.min];
const mid = positions[positionIds.mid];
const max = positions[positionIds.max];
if (!min || !mid || !max) return;
// We populate two paint arrays because, for cross-faded properties, we don't know which direction // We populate two paint arrays because, for cross-faded properties, we don't know which direction
// we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass // we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass
// unnecessary vertex data to the shaders, we determine which to upload at draw time. // unnecessary vertex data to the shaders, we determine which to upload at draw time.
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
this.zoomInPaintVertexArray.emplace(i, this.emplace(this.zoomInPaintVertexArray, i, mid, min);
imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1], this.emplace(this.zoomOutPaintVertexArray, i, mid, max);
imageMin.tl[0], imageMin.tl[1], imageMin.br[0], imageMin.br[1],
imageMid.pixelRatio,
imageMin.pixelRatio,
);
this.zoomOutPaintVertexArray.emplace(i,
imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1],
imageMax.tl[0], imageMax.tl[1], imageMax.br[0], imageMax.br[1],
imageMid.pixelRatio,
imageMax.pixelRatio,
);
} }
} }
upload(context: Context) { upload(context: Context) {
if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) { if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) {
this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); const attributes = this.getVertexAttributes();
this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, attributes, this.expression.isStateDependent);
this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, attributes, this.expression.isStateDependent);
} }
} }
@ -399,6 +420,50 @@ class CrossFadedCompositeBinder implements AttributeBinder {
} }
} }
class CrossFadedPatternBinder extends CrossFadedBinder<ImagePosition> {
protected getPositions(options: PaintOptions): {[_: string]: ImagePosition} {
return options.imagePositions;
}
protected getPositionIds(feature: Feature) {
return feature.patterns && feature.patterns[this.layerId];
}
getVertexAttributes(): Array<StructArrayMember> {
return patternAttributes.members;
}
protected emplace(array: StructArray, index: number, midPos: ImagePosition, minMaxPos: ImagePosition): void {
array.emplace(index,
midPos.tlbr[0], midPos.tlbr[1], midPos.tlbr[2], midPos.tlbr[3],
minMaxPos.tlbr[0], minMaxPos.tlbr[1], minMaxPos.tlbr[2], minMaxPos.tlbr[3],
midPos.pixelRatio,
minMaxPos.pixelRatio,
);
}
}
class CrossFadedDasharrayBinder extends CrossFadedBinder<DashEntry> {
protected getPositions(options: PaintOptions): {[_: string]: DashEntry} {
return options.dashPositions;
}
protected getPositionIds(feature: Feature) {
return feature.dashes && feature.dashes[this.layerId];
}
getVertexAttributes(): Array<StructArrayMember> {
return dashAttributes.members;
}
protected emplace(array: StructArray, index: number, midPos: DashEntry, minMaxPos: DashEntry): void {
array.emplace(index,
0, midPos.y, midPos.height, midPos.width,
0, minMaxPos.y, minMaxPos.height, minMaxPos.width,
);
}
}
/** /**
* @internal * @internal
* ProgramConfiguration contains the logic for binding style layer properties and tile * ProgramConfiguration contains the logic for binding style layer properties and tile
@ -452,7 +517,9 @@ export class ProgramConfiguration {
} else if (expression.kind === 'source' || isCrossFaded) { } else if (expression.kind === 'source' || isCrossFaded) {
const StructArrayLayout = layoutType(property, type, 'source'); const StructArrayLayout = layoutType(property, type, 'source');
this.binders[property] = isCrossFaded ? this.binders[property] = isCrossFaded ?
new CrossFadedCompositeBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) : property === 'line-dasharray' ?
new CrossFadedDasharrayBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new CrossFadedPatternBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new SourceExpressionBinder(expression as SourceExpression, names, type, StructArrayLayout); new SourceExpressionBinder(expression as SourceExpression, names, type, StructArrayLayout);
keys.push(`/a_${property}`); keys.push(`/a_${property}`);
@ -474,8 +541,8 @@ export class ProgramConfiguration {
populatePaintArrays(newLength: number, feature: Feature, options: PaintOptions) { populatePaintArrays(newLength: number, feature: Feature, options: PaintOptions) {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
(binder as AttributeBinder).populatePaintArray(newLength, feature, options); binder.populatePaintArray(newLength, feature, options);
} }
} }
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) { setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) {
@ -486,6 +553,14 @@ export class ProgramConfiguration {
} }
} }
setConstantDashPositions(dashTo: DashEntry, dashFrom: DashEntry) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof CrossFadedConstantBinder)
binder.setConstantDashPositions(dashTo, dashFrom);
}
}
updatePaintArrays( updatePaintArrays(
featureStates: FeatureStates, featureStates: FeatureStates,
featureMap: FeaturePositionMap, featureMap: FeaturePositionMap,
@ -503,11 +578,11 @@ export class ProgramConfiguration {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ||
binder instanceof CrossFadedCompositeBinder) && (binder as any).expression.isStateDependent === true) { binder instanceof CrossFadedBinder) && binder.expression.isStateDependent === true) {
//AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255 //AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255
const value = (layer.paint as any).get(property); const value = (layer.paint as any).get(property);
(binder as any).expression = value.value; binder.expression = value.value;
(binder as AttributeBinder).updatePaintArray(pos.start, pos.end, feature, featureStates[id], options); binder.updatePaintArray(pos.start, pos.end, feature, featureStates[id], options);
dirty = true; dirty = true;
} }
} }
@ -535,9 +610,10 @@ export class ProgramConfiguration {
for (let i = 0; i < binder.paintVertexAttributes.length; i++) { for (let i = 0; i < binder.paintVertexAttributes.length; i++) {
result.push(binder.paintVertexAttributes[i].name); result.push(binder.paintVertexAttributes[i].name);
} }
} else if (binder instanceof CrossFadedCompositeBinder) { } else if (binder instanceof CrossFadedBinder) {
for (let i = 0; i < patternAttributes.members.length; i++) { const attributes = binder.getVertexAttributes();
result.push(patternAttributes.members[i].name); for (const attribute of attributes) {
result.push(attribute.name);
} }
} }
} }
@ -595,7 +671,7 @@ export class ProgramConfiguration {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (crossfade && binder instanceof CrossFadedCompositeBinder) { if (crossfade && binder instanceof CrossFadedBinder) {
const patternVertexBuffer = crossfade.fromScale === 2 ? binder.zoomInPaintVertexBuffer : binder.zoomOutPaintVertexBuffer; const patternVertexBuffer = crossfade.fromScale === 2 ? binder.zoomInPaintVertexBuffer : binder.zoomOutPaintVertexBuffer;
if (patternVertexBuffer) this._buffers.push(patternVertexBuffer); if (patternVertexBuffer) this._buffers.push(patternVertexBuffer);
@ -608,7 +684,7 @@ export class ProgramConfiguration {
upload(context: Context) { upload(context: Context) {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.upload(context); binder.upload(context);
} }
this.updatePaintBuffers(); this.updatePaintBuffers();
@ -617,7 +693,7 @@ export class ProgramConfiguration {
destroy() { destroy() {
for (const property in this.binders) { for (const property in this.binders) {
const binder = this.binders[property]; const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.destroy(); binder.destroy();
} }
} }
@ -677,7 +753,7 @@ export class ProgramConfigurationSet<Layer extends TypedStyleLayer> {
} }
} }
function paintAttributeNames(property, type) { function paintAttributeNames(property: string, type: string) {
const attributeNameExceptions = { const attributeNameExceptions = {
'text-opacity': ['opacity'], 'text-opacity': ['opacity'],
'icon-opacity': ['opacity'], 'icon-opacity': ['opacity'],
@ -690,6 +766,7 @@ function paintAttributeNames(property, type) {
'text-halo-width': ['halo_width'], 'text-halo-width': ['halo_width'],
'icon-halo-width': ['halo_width'], 'icon-halo-width': ['halo_width'],
'line-gap-width': ['gapwidth'], 'line-gap-width': ['gapwidth'],
'line-dasharray': ['dasharray_to', 'dasharray_from'],
'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
@ -698,7 +775,7 @@ function paintAttributeNames(property, type) {
return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')]; return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')];
} }
function getLayoutException(property) { function getLayoutException(property: string) {
const propertyExceptions = { const propertyExceptions = {
'line-pattern': { 'line-pattern': {
'source': PatternLayoutArray, 'source': PatternLayoutArray,
@ -711,13 +788,17 @@ function getLayoutException(property) {
'fill-extrusion-pattern': { 'fill-extrusion-pattern': {
'source': PatternLayoutArray, 'source': PatternLayoutArray,
'composite': PatternLayoutArray 'composite': PatternLayoutArray
} },
'line-dasharray': {
'source': DashLayoutArray,
'composite': DashLayoutArray
},
}; };
return propertyExceptions[property]; return propertyExceptions[property];
} }
function layoutType(property, type, binderType) { function layoutType(property: string, type: string, binderType: string) {
const defaultLayouts = { const defaultLayouts = {
'color': { 'color': {
'source': StructArrayLayout2f8, 'source': StructArrayLayout2f8,
@ -736,7 +817,8 @@ function layoutType(property, type, binderType) {
register('ConstantBinder', ConstantBinder); register('ConstantBinder', ConstantBinder);
register('CrossFadedConstantBinder', CrossFadedConstantBinder); register('CrossFadedConstantBinder', CrossFadedConstantBinder);
register('SourceExpressionBinder', SourceExpressionBinder); register('SourceExpressionBinder', SourceExpressionBinder);
register('CrossFadedCompositeBinder', CrossFadedCompositeBinder); register('CrossFadedPatternBinder', CrossFadedPatternBinder);
register('CrossFadedDasharrayBinder', CrossFadedDasharrayBinder);
register('CompositeExpressionBinder', CompositeExpressionBinder); register('CompositeExpressionBinder', CompositeExpressionBinder);
register('ProgramConfiguration', ProgramConfiguration, {omit: ['_buffers']}); register('ProgramConfiguration', ProgramConfiguration, {omit: ['_buffers']});
register('ProgramConfigurationSet', ProgramConfigurationSet); register('ProgramConfigurationSet', ProgramConfigurationSet);

View File

@ -289,7 +289,7 @@ describe('coveringTiles', () => {
tileSize: 512 tileSize: 512
}; };
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(200, 200); transform.resize(200, 200);
test('general', () => { test('general', () => {
@ -531,7 +531,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 10, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 10, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(10, 400); transform.resize(10, 400);
// make slightly off center so that sort order is not subject to precision issues // make slightly off center so that sort order is not subject to precision issues
transform.setCenter(new LngLat(-0.01, 0.01)); transform.setCenter(new LngLat(-0.01, 0.01));
@ -552,7 +552,7 @@ describe('coveringTiles', () => {
tileSize: 512 tileSize: 512
}; };
const transform = new MercatorTransform(0, 0, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 0, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(200, 200); transform.resize(200, 200);
transform.setCenter(new LngLat(0.01, 0.01)); transform.setCenter(new LngLat(0.01, 0.01));
transform.setZoom(8); transform.setZoom(8);
@ -569,7 +569,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, -0.087)); transform.setCenter(new LngLat(-179.73, -0.087));
@ -587,7 +587,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, 60.02)); transform.setCenter(new LngLat(-179.73, 60.02));
@ -605,7 +605,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, 85.028)); transform.setCenter(new LngLat(-179.73, 85.028));
@ -623,7 +623,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(-58.97, 60.02)); transform.setCenter(new LngLat(-58.97, 60.02));
@ -641,7 +641,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(-58.97, -0.087)); transform.setCenter(new LngLat(-58.97, -0.087));
@ -659,7 +659,7 @@ describe('coveringTiles', () => {
reparseOverscaled: true reparseOverscaled: true
}; };
const transform = new MercatorTransform(0, 15, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128); transform.resize(128, 128);
transform.setZoom(11); transform.setZoom(11);
transform.setCenter(new LngLat(0.03, 0.0915)); transform.setCenter(new LngLat(0.03, 0.0915));
@ -679,7 +679,7 @@ describe('coveringZoomLevel', () => {
let options: CoveringTilesOptions; let options: CoveringTilesOptions;
beforeEach(() => { beforeEach(() => {
transform = new MercatorTransform(0, 22, 0, 60, true); transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
options = { options = {
tileSize: 512, tileSize: 512,
roundZoom: false, roundZoom: false,

View File

@ -597,7 +597,7 @@ describe('GlobeTransform', () => {
test('change transform and make sure render world copies is kept', () => { test('change transform and make sure render world copies is kept', () => {
const globeTransform = createGlobeTransform(); const globeTransform = createGlobeTransform();
globeTransform.setRenderWorldCopies(true); globeTransform.setRenderWorldCopies(true);
const mercator = new MercatorTransform(0, 1, 2, 3, false); const mercator = new MercatorTransform({minZoom: 0, maxZoom: 1, minPitch: 2, maxPitch: 3, renderWorldCopies: false});
mercator.apply(globeTransform); mercator.apply(globeTransform);
expect(mercator.renderWorldCopies).toBeTruthy(); expect(mercator.renderWorldCopies).toBeTruthy();

View File

@ -12,7 +12,8 @@ import type {LngLatBounds} from '../lng_lat_bounds';
import type {Frustum} from '../../util/primitives/frustum'; import type {Frustum} from '../../util/primitives/frustum';
import type {Terrain} from '../../render/terrain'; import type {Terrain} from '../../render/terrain';
import type {PointProjection} from '../../symbol/projection'; import type {PointProjection} from '../../symbol/projection';
import type {IReadonlyTransform, ITransform} from '../transform_interface'; import type {IReadonlyTransform, ITransform, TransformConstrainFunction} from '../transform_interface';
import type {TransformOptions} from '../transform_helper';
import type {PaddingOptions} from '../edge_insets'; import type {PaddingOptions} from '../edge_insets';
import type {ProjectionData, ProjectionDataParams} from './projection_data'; import type {ProjectionData, ProjectionDataParams} from './projection_data';
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider'; import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
@ -108,6 +109,9 @@ export class GlobeTransform implements ITransform {
setMaxBounds(bounds?: LngLatBounds): void { setMaxBounds(bounds?: LngLatBounds): void {
this._helper.setMaxBounds(bounds); this._helper.setMaxBounds(bounds);
} }
setConstrain(constrain?: TransformConstrainFunction | null): void {
this._helper.setConstrain(constrain);
}
overrideNearFarZ(nearZ: number, farZ: number): void { overrideNearFarZ(nearZ: number, farZ: number): void {
this._helper.overrideNearFarZ(nearZ, farZ); this._helper.overrideNearFarZ(nearZ, farZ);
} }
@ -202,6 +206,9 @@ export class GlobeTransform implements ITransform {
get cameraToCenterDistance(): number { get cameraToCenterDistance(): number {
return this._helper.cameraToCenterDistance; return this._helper.cameraToCenterDistance;
} }
get constrain(): TransformConstrainFunction {
return this._helper.constrain;
}
public get nearZ(): number { public get nearZ(): number {
return this._helper.nearZ; return this._helper.nearZ;
} }
@ -246,11 +253,11 @@ export class GlobeTransform implements ITransform {
private _mercatorTransform: MercatorTransform; private _mercatorTransform: MercatorTransform;
private _verticalPerspectiveTransform: VerticalPerspectiveTransform; private _verticalPerspectiveTransform: VerticalPerspectiveTransform;
public constructor() { public constructor(options?: TransformOptions) {
this._helper = new TransformHelper({ this._helper = new TransformHelper({
calcMatrices: () => { this._calcMatrices(); }, calcMatrices: () => { this._calcMatrices(); },
getConstrained: (center, zoom) => { return this.getConstrained(center, zoom); } constrain: (center, zoom) => { return this.defaultConstrain(center, zoom); }
}); }, options);
this._globeness = 1; // When transform is cloned for use in symbols, `_updateAnimation` function which usually sets this value never gets called. this._globeness = 1; // When transform is cloned for use in symbols, `_updateAnimation` function which usually sets this value never gets called.
this._mercatorTransform = new MercatorTransform(); this._mercatorTransform = new MercatorTransform();
this._verticalPerspectiveTransform = new VerticalPerspectiveTransform(); this._verticalPerspectiveTransform = new VerticalPerspectiveTransform();
@ -392,9 +399,9 @@ export class GlobeTransform implements ITransform {
return this.currentTransform.getBounds(); return this.currentTransform.getBounds();
} }
getConstrained(lngLat: LngLat, zoom: number): { center: LngLat; zoom: number } { defaultConstrain: TransformConstrainFunction = (lngLat, zoom) => {
return this.currentTransform.getConstrained(lngLat, zoom); return this.currentTransform.defaultConstrain(lngLat, zoom);
} };
calculateCenterFromCameraLngLatAlt(lngLat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} { calculateCenterFromCameraLngLatAlt(lngLat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} {
return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch); return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch);

View File

@ -87,7 +87,7 @@ export class MercatorCameraHelper implements ICameraHelper {
let pointAtOffset = tr.centerPoint.add(options.offsetAsPoint); let pointAtOffset = tr.centerPoint.add(options.offsetAsPoint);
const locationAtOffset = tr.screenPointToLocation(pointAtOffset); const locationAtOffset = tr.screenPointToLocation(pointAtOffset);
const {center, zoom: endZoom} = tr.getConstrained( const {center, zoom: endZoom} = tr.constrain(
LngLat.convert(options.center || locationAtOffset), LngLat.convert(options.center || locationAtOffset),
zoom ?? startZoom zoom ?? startZoom
); );
@ -144,7 +144,7 @@ export class MercatorCameraHelper implements ICameraHelper {
const startZoom = tr.zoom; const startZoom = tr.zoom;
// Obtain target center and zoom // Obtain target center and zoom
const constrained = tr.getConstrained( const constrained = tr.constrain(
LngLat.convert(options.center || options.locationAtOffset), LngLat.convert(options.center || options.locationAtOffset),
optionsZoom ? +options.zoom : startZoom optionsZoom ? +options.zoom : startZoom
); );
@ -166,7 +166,7 @@ export class MercatorCameraHelper implements ICameraHelper {
if (optionsMinZoom) { if (optionsMinZoom) {
const minZoomPreConstrain = Math.min(+options.minZoom, startZoom, targetZoom); const minZoomPreConstrain = Math.min(+options.minZoom, startZoom, targetZoom);
const minZoom = tr.getConstrained(targetCenter, minZoomPreConstrain).zoom; const minZoom = tr.constrain(targetCenter, minZoomPreConstrain).zoom;
scaleOfMinZoom = zoomScale(minZoom - startZoom); scaleOfMinZoom = zoomScale(minZoom - startZoom);
} }

View File

@ -12,7 +12,7 @@ import {expectToBeCloseToArray} from '../../util/test/util';
describe('transform', () => { describe('transform', () => {
test('creates a transform', () => { test('creates a transform', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
expect(transform.unmodified).toBe(true); expect(transform.unmodified).toBe(true);
expect(transform.tileSize).toBe(512); expect(transform.tileSize).toBe(512);
@ -60,14 +60,14 @@ describe('transform', () => {
test('does not throw on bad center', () => { test('does not throw on bad center', () => {
expect(() => { expect(() => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setCenter(new LngLat(50, -90)); transform.setCenter(new LngLat(50, -90));
}).not.toThrow(); }).not.toThrow();
}); });
test('setLocationAt', () => { test('setLocationAt', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setZoom(4); transform.setZoom(4);
expect(transform.center).toEqual({lng: 0, lat: 0}); expect(transform.center).toEqual({lng: 0, lat: 0});
@ -76,7 +76,7 @@ describe('transform', () => {
}); });
test('setLocationAt tilted', () => { test('setLocationAt tilted', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setZoom(4); transform.setZoom(4);
transform.setPitch(50); transform.setPitch(50);
@ -86,7 +86,7 @@ describe('transform', () => {
}); });
test('setLocationAt tilted rolled', () => { test('setLocationAt tilted rolled', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setZoom(4); transform.setZoom(4);
transform.setPitch(50); transform.setPitch(50);
@ -97,26 +97,26 @@ describe('transform', () => {
}); });
test('has a default zoom', () => { test('has a default zoom', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
expect(transform.tileZoom).toBe(0); expect(transform.tileZoom).toBe(0);
expect(transform.tileZoom).toBe(transform.zoom); expect(transform.tileZoom).toBe(transform.zoom);
}); });
test('set zoom inits tileZoom with zoom value', () => { test('set zoom inits tileZoom with zoom value', () => {
const transform = new MercatorTransform(0, 22, 0, 60); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60});
transform.setZoom(5); transform.setZoom(5);
expect(transform.tileZoom).toBe(5); expect(transform.tileZoom).toBe(5);
}); });
test('set zoom clamps tileZoom to non negative value ', () => { test('set zoom clamps tileZoom to non negative value ', () => {
const transform = new MercatorTransform(-2, 22, 0, 60); const transform = new MercatorTransform({minZoom: -2, maxZoom: 22, minPitch: 0, maxPitch: 60});
transform.setZoom(-2); transform.setZoom(-2);
expect(transform.tileZoom).toBe(0); expect(transform.tileZoom).toBe(0);
}); });
test('set fov', () => { test('set fov', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setFov(10); transform.setFov(10);
expect(transform.fov).toBe(10); expect(transform.fov).toBe(10);
transform.setFov(10); transform.setFov(10);
@ -124,7 +124,7 @@ describe('transform', () => {
}); });
test('lngRange & latRange constrain zoom and center', () => { test('lngRange & latRange constrain zoom and center', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setCenter(new LngLat(0, 0)); transform.setCenter(new LngLat(0, 0));
transform.setZoom(10); transform.setZoom(10);
transform.resize(500, 500); transform.resize(500, 500);
@ -143,7 +143,7 @@ describe('transform', () => {
}); });
test('lngRange & latRange constrain zoom and center after cloning', () => { test('lngRange & latRange constrain zoom and center after cloning', () => {
const old = new MercatorTransform(0, 22, 0, 60, true); const old = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
old.setCenter(new LngLat(0, 0)); old.setCenter(new LngLat(0, 0));
old.setZoom(10); old.setZoom(10);
old.resize(500, 500); old.resize(500, 500);
@ -164,7 +164,7 @@ describe('transform', () => {
}); });
test('lngRange can constrain zoom and center across meridian', () => { test('lngRange can constrain zoom and center across meridian', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setCenter(new LngLat(180, 0)); transform.setCenter(new LngLat(180, 0));
transform.setZoom(10); transform.setZoom(10);
transform.resize(500, 500); transform.resize(500, 500);
@ -196,7 +196,7 @@ describe('transform', () => {
}); });
test('clamps pitch', () => { test('clamps pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setPitch(45); transform.setPitch(45);
expect(transform.pitch).toBe(45); expect(transform.pitch).toBe(45);
@ -209,7 +209,7 @@ describe('transform', () => {
}); });
test('visibleUnwrappedCoordinates', () => { test('visibleUnwrappedCoordinates', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(200, 200); transform.resize(200, 200);
transform.setZoom(0); transform.setZoom(0);
transform.setCenter(new LngLat(-170.01, 0.01)); transform.setCenter(new LngLat(-170.01, 0.01));
@ -224,7 +224,7 @@ describe('transform', () => {
}); });
test('maintains high float precision when calculating matrices', () => { test('maintains high float precision when calculating matrices', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(200.25, 200.25); transform.resize(200.25, 200.25);
transform.setZoom(20.25); transform.setZoom(20.25);
transform.setPitch(67.25); transform.setPitch(67.25);
@ -237,7 +237,7 @@ describe('transform', () => {
}); });
test('recalculateZoomAndCenter: no change', () => { test('recalculateZoomAndCenter: no change', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setElevation(200); transform.setElevation(200);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
transform.setZoom(14); transform.setZoom(14);
@ -264,7 +264,7 @@ describe('transform', () => {
}); });
test('recalculateZoomAndCenter: elevation increase', () => { test('recalculateZoomAndCenter: elevation increase', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setElevation(200); transform.setElevation(200);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
transform.setZoom(14); transform.setZoom(14);
@ -296,7 +296,7 @@ describe('transform', () => {
}); });
test('recalculateZoomAndCenter: elevation decrease', () => { test('recalculateZoomAndCenter: elevation decrease', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setElevation(200); transform.setElevation(200);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
transform.setZoom(14); transform.setZoom(14);
@ -326,7 +326,7 @@ describe('transform', () => {
}); });
test('recalculateZoomAndCenterNoTerrain', () => { test('recalculateZoomAndCenterNoTerrain', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setElevation(200); transform.setElevation(200);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
transform.setZoom(14); transform.setZoom(14);
@ -354,7 +354,7 @@ describe('transform', () => {
}); });
test('pointCoordinate with terrain when returning null should fall back to 2D', () => { test('pointCoordinate with terrain when returning null should fall back to 2D', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
const terrain = { const terrain = {
pointCoordinate: () => null pointCoordinate: () => null
@ -365,7 +365,7 @@ describe('transform', () => {
}); });
test('getBounds with horizon', () => { test('getBounds with horizon', () => {
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setPitch(60); transform.setPitch(60);
@ -378,7 +378,7 @@ describe('transform', () => {
}); });
test('lngLatToCameraDepth', () => { test('lngLatToCameraDepth', () => {
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
@ -389,7 +389,7 @@ describe('transform', () => {
test('projectTileCoordinates', () => { test('projectTileCoordinates', () => {
const precisionDigits = 10; const precisionDigits = 10;
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setCenter(new LngLat(10.0, 50.0)); transform.setCenter(new LngLat(10.0, 50.0));
let projection = transform.projectTileCoordinates(1024, 1024, new UnwrappedTileID(0, new CanonicalTileID(1, 1, 0)), (_x, _y) => 0); let projection = transform.projectTileCoordinates(1024, 1024, new UnwrappedTileID(0, new CanonicalTileID(1, 1, 0)), (_x, _y) => 0);
@ -407,7 +407,7 @@ describe('transform', () => {
}); });
test('getCameraLngLat', () => { test('getCameraLngLat', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setElevation(200); transform.setElevation(200);
transform.setCenter(new LngLat(15.0, 55.0)); transform.setCenter(new LngLat(15.0, 55.0));
transform.setZoom(14); transform.setZoom(14);
@ -427,7 +427,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt no pitch no bearing', () => { test('calculateCenterFromCameraLngLatAlt no pitch no bearing', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -445,7 +445,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt no pitch', () => { test('calculateCenterFromCameraLngLatAlt no pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -465,7 +465,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt', () => { test('calculateCenterFromCameraLngLatAlt', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -487,7 +487,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt 89 degrees pitch', () => { test('calculateCenterFromCameraLngLatAlt 89 degrees pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -509,7 +509,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt 89.99 degrees pitch', () => { test('calculateCenterFromCameraLngLatAlt 89.99 degrees pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -531,7 +531,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt 90 degrees pitch', () => { test('calculateCenterFromCameraLngLatAlt 90 degrees pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -553,7 +553,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt 95 degrees pitch', () => { test('calculateCenterFromCameraLngLatAlt 95 degrees pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);
@ -575,7 +575,7 @@ describe('transform', () => {
}); });
test('calculateCenterFromCameraLngLatAlt 180 degrees pitch', () => { test('calculateCenterFromCameraLngLatAlt 180 degrees pitch', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.setPitch(55); transform.setPitch(55);
transform.setBearing(75); transform.setBearing(75);
transform.resize(512, 512); transform.resize(512, 512);

View File

@ -14,7 +14,8 @@ import {MercatorCoveringTilesDetailsProvider} from './mercator_covering_tiles_de
import {Frustum} from '../../util/primitives/frustum'; import {Frustum} from '../../util/primitives/frustum';
import type {Terrain} from '../../render/terrain'; import type {Terrain} from '../../render/terrain';
import type {IReadonlyTransform, ITransform} from '../transform_interface'; import type {IReadonlyTransform, ITransform, TransformConstrainFunction} from '../transform_interface';
import type {TransformOptions} from '../transform_helper';
import type {PaddingOptions} from '../edge_insets'; import type {PaddingOptions} from '../edge_insets';
import type {ProjectionData, ProjectionDataParams} from './projection_data'; import type {ProjectionData, ProjectionDataParams} from './projection_data';
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider'; import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
@ -107,6 +108,9 @@ export class MercatorTransform implements ITransform {
setMaxBounds(bounds?: LngLatBounds): void { setMaxBounds(bounds?: LngLatBounds): void {
this._helper.setMaxBounds(bounds); this._helper.setMaxBounds(bounds);
} }
setConstrain(constrain?: TransformConstrainFunction | null): void {
this._helper.setConstrain(constrain);
}
overrideNearFarZ(nearZ: number, farZ: number): void { overrideNearFarZ(nearZ: number, farZ: number): void {
this._helper.overrideNearFarZ(nearZ, farZ); this._helper.overrideNearFarZ(nearZ, farZ);
} }
@ -201,6 +205,9 @@ export class MercatorTransform implements ITransform {
get cameraToCenterDistance(): number { get cameraToCenterDistance(): number {
return this._helper.cameraToCenterDistance; return this._helper.cameraToCenterDistance;
} }
get constrain(): TransformConstrainFunction {
return this._helper.constrain;
}
public get nearZ(): number { public get nearZ(): number {
return this._helper.nearZ; return this._helper.nearZ;
} }
@ -236,11 +243,11 @@ export class MercatorTransform implements ITransform {
private _coveringTilesDetailsProvider; private _coveringTilesDetailsProvider;
constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) { constructor(options?: TransformOptions) {
this._helper = new TransformHelper({ this._helper = new TransformHelper({
calcMatrices: () => { this._calcMatrices(); }, calcMatrices: () => { this._calcMatrices(); },
getConstrained: (center, zoom) => { return this.getConstrained(center, zoom); } constrain: (center, zoom) => { return this.defaultConstrain(center, zoom); }
}, minZoom, maxZoom, minPitch, maxPitch, renderWorldCopies); }, options);
this._coveringTilesDetailsProvider = new MercatorCoveringTilesDetailsProvider(); this._coveringTilesDetailsProvider = new MercatorCoveringTilesDetailsProvider();
} }
@ -444,7 +451,7 @@ export class MercatorTransform implements ITransform {
* *
* Bounds are those set by maxBounds or North & South "Poles" and, if only 1 globe is displayed, antimeridian. * Bounds are those set by maxBounds or North & South "Poles" and, if only 1 globe is displayed, antimeridian.
*/ */
getConstrained(lngLat: LngLat, zoom: number): {center: LngLat; zoom: number} { defaultConstrain: TransformConstrainFunction = (lngLat, zoom) => {
zoom = clamp(+zoom, this.minZoom, this.maxZoom); zoom = clamp(+zoom, this.minZoom, this.maxZoom);
const result = { const result = {
center: new LngLat(lngLat.lng, lngLat.lat), center: new LngLat(lngLat.lng, lngLat.lat),
@ -533,7 +540,7 @@ export class MercatorTransform implements ITransform {
} }
return result; return result;
} };
calculateCenterFromCameraLngLatAlt(lnglat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} { calculateCenterFromCameraLngLatAlt(lnglat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} {
return this._helper.calculateCenterFromCameraLngLatAlt(lnglat, alt, bearing, pitch); return this._helper.calculateCenterFromCameraLngLatAlt(lnglat, alt, bearing, pitch);

View File

@ -9,20 +9,20 @@ import {createIdentityMat4f32, MAX_VALID_LATITUDE} from '../../util/util';
describe('mercator utils', () => { describe('mercator utils', () => {
test('projectToWorldCoordinates basic', () => { test('projectToWorldCoordinates basic', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.setZoom(10); transform.setZoom(10);
expect(projectToWorldCoordinates(transform.worldSize, transform.center)).toEqual(new Point(262144, 262144)); expect(projectToWorldCoordinates(transform.worldSize, transform.center)).toEqual(new Point(262144, 262144));
}); });
test('projectToWorldCoordinates clamps latitude', () => { test('projectToWorldCoordinates clamps latitude', () => {
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
expect(projectToWorldCoordinates(transform.worldSize, new LngLat(0, -90))).toEqual(projectToWorldCoordinates(transform.worldSize, new LngLat(0, -MAX_VALID_LATITUDE))); expect(projectToWorldCoordinates(transform.worldSize, new LngLat(0, -90))).toEqual(projectToWorldCoordinates(transform.worldSize, new LngLat(0, -MAX_VALID_LATITUDE)));
expect(projectToWorldCoordinates(transform.worldSize, new LngLat(0, 90))).toEqual(projectToWorldCoordinates(transform.worldSize, new LngLat(0, MAX_VALID_LATITUDE))); expect(projectToWorldCoordinates(transform.worldSize, new LngLat(0, 90))).toEqual(projectToWorldCoordinates(transform.worldSize, new LngLat(0, MAX_VALID_LATITUDE)));
}); });
test('getMercatorHorizon', () => { test('getMercatorHorizon', () => {
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setPitch(75); transform.setPitch(75);
const horizon = getMercatorHorizon(transform); const horizon = getMercatorHorizon(transform);
@ -31,7 +31,7 @@ describe('mercator utils', () => {
}); });
test('getMercatorHorizon90', () => { test('getMercatorHorizon90', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setPitch(90); transform.setPitch(90);
const horizon = getMercatorHorizon(transform); const horizon = getMercatorHorizon(transform);
@ -40,7 +40,7 @@ describe('mercator utils', () => {
}); });
test('getMercatorHorizon95', () => { test('getMercatorHorizon95', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setPitch(95); transform.setPitch(95);
const horizon = getMercatorHorizon(transform); const horizon = getMercatorHorizon(transform);
@ -49,7 +49,7 @@ describe('mercator utils', () => {
}); });
describe('getProjectionData', () => { describe('getProjectionData', () => {
test('return identity matrix when not passing overscaledTileID', () => { test('return identity matrix when not passing overscaledTileID', () => {
const transform = new MercatorTransform(0, 22, 0, 180, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 180, renderWorldCopies: true});
const projectionData = transform.getProjectionData({overscaledTileID: null}); const projectionData = transform.getProjectionData({overscaledTileID: null});
expect(projectionData.fallbackMatrix).toEqual(createIdentityMat4f32()); expect(projectionData.fallbackMatrix).toEqual(createIdentityMat4f32());
}); });

View File

@ -11,19 +11,20 @@ import {VerticalPerspectiveProjection} from './vertical_perspective_projection';
import type {ProjectionSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {ProjectionSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {Projection} from './projection'; import type {Projection} from './projection';
import type {ITransform} from '../transform_interface'; import type {ITransform, TransformConstrainFunction} from '../transform_interface';
import type {ICameraHelper} from './camera_helper'; import type {ICameraHelper} from './camera_helper';
export function createProjectionFromName(name: ProjectionSpecification['type']): { export function createProjectionFromName(name: ProjectionSpecification['type'], transformConstrain?: TransformConstrainFunction): {
projection: Projection; projection: Projection;
transform: ITransform; transform: ITransform;
cameraHelper: ICameraHelper; cameraHelper: ICameraHelper;
} { } {
const transformOptions = {constrain: transformConstrain};
if (Array.isArray(name)) { if (Array.isArray(name)) {
const globeProjection = new GlobeProjection({type: name}); const globeProjection = new GlobeProjection({type: name});
return { return {
projection: globeProjection, projection: globeProjection,
transform: new GlobeTransform(), transform: new GlobeTransform(transformOptions),
cameraHelper: new GlobeCameraHelper(globeProjection), cameraHelper: new GlobeCameraHelper(globeProjection),
}; };
} }
@ -32,7 +33,7 @@ export function createProjectionFromName(name: ProjectionSpecification['type']):
{ {
return { return {
projection: new MercatorProjection(), projection: new MercatorProjection(),
transform: new MercatorTransform(), transform: new MercatorTransform(transformOptions),
cameraHelper: new MercatorCameraHelper(), cameraHelper: new MercatorCameraHelper(),
}; };
} }
@ -49,7 +50,7 @@ export function createProjectionFromName(name: ProjectionSpecification['type']):
]}); ]});
return { return {
projection: globeProjection, projection: globeProjection,
transform: new GlobeTransform(), transform: new GlobeTransform(transformOptions),
cameraHelper: new GlobeCameraHelper(globeProjection), cameraHelper: new GlobeCameraHelper(globeProjection),
}; };
} }
@ -57,7 +58,7 @@ export function createProjectionFromName(name: ProjectionSpecification['type']):
{ {
return { return {
projection: new VerticalPerspectiveProjection(), projection: new VerticalPerspectiveProjection(),
transform: new VerticalPerspectiveTransform(), transform: new VerticalPerspectiveTransform(transformOptions),
cameraHelper: new VerticalPerspectiveCameraHelper(), cameraHelper: new VerticalPerspectiveCameraHelper(),
}; };
} }
@ -66,7 +67,7 @@ export function createProjectionFromName(name: ProjectionSpecification['type']):
warnOnce(`Unknown projection name: ${name}. Falling back to mercator projection.`); warnOnce(`Unknown projection name: ${name}. Falling back to mercator projection.`);
return { return {
projection: new MercatorProjection(), projection: new MercatorProjection(),
transform: new MercatorTransform(), transform: new MercatorTransform(transformOptions),
cameraHelper: new MercatorCameraHelper(), cameraHelper: new MercatorCameraHelper(),
}; };
} }

View File

@ -213,7 +213,7 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
// Special zoom & center handling for globe: // Special zoom & center handling for globe:
// Globe constrained center isn't dependent on zoom level // Globe constrained center isn't dependent on zoom level
const startingLat = tr.center.lat; const startingLat = tr.center.lat;
const constrainedCenter = tr.getConstrained(options.center ? LngLat.convert(options.center) : tr.center, tr.zoom).center; const constrainedCenter = tr.constrain(options.center ? LngLat.convert(options.center) : tr.center, tr.zoom).center;
tr.setCenter(constrainedCenter.wrap()); tr.setCenter(constrainedCenter.wrap());
// Make sure to compute correct target zoom level if no zoom is specified // Make sure to compute correct target zoom level if no zoom is specified
@ -245,7 +245,7 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
const preConstrainCenter = options.center ? const preConstrainCenter = options.center ?
LngLat.convert(options.center) : LngLat.convert(options.center) :
startCenter; startCenter;
const constrainedCenter = tr.getConstrained( const constrainedCenter = tr.constrain(
preConstrainCenter, preConstrainCenter,
startZoom // zoom can be whatever at this stage, it should not affect anything if globe is enabled startZoom // zoom can be whatever at this stage, it should not affect anything if globe is enabled
).center; ).center;
@ -334,7 +334,7 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
const doPadding = !tr.isPaddingEqual(options.padding); const doPadding = !tr.isPaddingEqual(options.padding);
// Obtain target center and zoom // Obtain target center and zoom
const constrainedCenter = tr.getConstrained( const constrainedCenter = tr.constrain(
LngLat.convert(options.center || options.locationAtOffset), LngLat.convert(options.center || options.locationAtOffset),
startZoom startZoom
).center; ).center;
@ -369,7 +369,7 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
const normalizedOptionsMinZoom = +options.minZoom + getZoomAdjustment(targetCenter.lat, 0); const normalizedOptionsMinZoom = +options.minZoom + getZoomAdjustment(targetCenter.lat, 0);
const normalizedMinZoomPreConstrain = Math.min(normalizedOptionsMinZoom, normalizedStartZoom, normalizedTargetZoom); const normalizedMinZoomPreConstrain = Math.min(normalizedOptionsMinZoom, normalizedStartZoom, normalizedTargetZoom);
const minZoomPreConstrain = normalizedMinZoomPreConstrain + getZoomAdjustment(0, targetCenter.lat); const minZoomPreConstrain = normalizedMinZoomPreConstrain + getZoomAdjustment(0, targetCenter.lat);
const minZoom = tr.getConstrained(targetCenter, minZoomPreConstrain).zoom; const minZoom = tr.constrain(targetCenter, minZoomPreConstrain).zoom;
const normalizedMinZoom = minZoom + getZoomAdjustment(targetCenter.lat, 0); const normalizedMinZoom = minZoom + getZoomAdjustment(targetCenter.lat, 0);
scaleOfMinZoom = zoomScale(normalizedMinZoom - normalizedStartZoom); scaleOfMinZoom = zoomScale(normalizedMinZoom - normalizedStartZoom);
} }

View File

@ -1,7 +1,7 @@
import type {Context} from '../../gl/context'; import type {Context} from '../../gl/context';
import type {CanonicalTileID} from '../../source/tile_id'; import type {CanonicalTileID} from '../../source/tile_id';
import {type Mesh} from '../../render/mesh'; import {type Mesh} from '../../render/mesh';
import {browser} from '../../util/browser'; import {now} from '../../util/time_control';
import {easeCubicInOut, lerp} from '../../util/util'; import {easeCubicInOut, lerp} from '../../util/util';
import {mercatorYfromLat} from '../mercator_coordinate'; import {mercatorYfromLat} from '../mercator_coordinate';
import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings'; import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
@ -103,15 +103,15 @@ export class VerticalPerspectiveProjection implements Projection {
const expectedResult = 2.0 * Math.atan(Math.exp(Math.PI - (mercatorY * Math.PI * 2.0))) - Math.PI * 0.5; const expectedResult = 2.0 * Math.atan(Math.exp(Math.PI - (mercatorY * Math.PI * 2.0))) - Math.PI * 0.5;
const newValue = this._errorMeasurement.updateErrorLoop(mercatorY, expectedResult); const newValue = this._errorMeasurement.updateErrorLoop(mercatorY, expectedResult);
const now = browser.now(); const currentTime = now();
if (newValue !== this._errorMeasurementLastValue) { if (newValue !== this._errorMeasurementLastValue) {
this._errorCorrectionPreviousValue = this._errorCorrectionUsable; // store the interpolated value this._errorCorrectionPreviousValue = this._errorCorrectionUsable; // store the interpolated value
this._errorMeasurementLastValue = newValue; this._errorMeasurementLastValue = newValue;
this._errorMeasurementLastChangeTime = now; this._errorMeasurementLastChangeTime = currentTime;
} }
const sinceUpdateSeconds = (now - this._errorMeasurementLastChangeTime) / 1000.0; const sinceUpdateSeconds = (currentTime - this._errorMeasurementLastChangeTime) / 1000.0;
const mix = Math.min(Math.max(sinceUpdateSeconds / globeConstants.errorTransitionTimeSeconds, 0.0), 1.0); const mix = Math.min(Math.max(sinceUpdateSeconds / globeConstants.errorTransitionTimeSeconds, 0.0), 1.0);
const newCorrection = -this._errorMeasurementLastValue; // Note the negation const newCorrection = -this._errorMeasurementLastValue; // Note the negation
this._errorCorrectionUsable = lerp(this._errorCorrectionPreviousValue, newCorrection, easeCubicInOut(mix)); this._errorCorrectionUsable = lerp(this._errorCorrectionPreviousValue, newCorrection, easeCubicInOut(mix));
@ -152,10 +152,10 @@ export class VerticalPerspectiveProjection implements Projection {
} }
hasTransition(): boolean { hasTransition(): boolean {
const now = browser.now(); const currentTime = now();
let dirty = false; let dirty = false;
// Error correction transition // Error correction transition
dirty = dirty || (now - this._errorMeasurementLastChangeTime) / 1000.0 < (globeConstants.errorTransitionTimeSeconds + 0.2); dirty = dirty || (currentTime - this._errorMeasurementLastChangeTime) / 1000.0 < (globeConstants.errorTransitionTimeSeconds + 0.2);
// Error correction query in flight // Error correction query in flight
dirty = dirty || (this._errorMeasurement && this._errorMeasurement.awaitingQuery); dirty = dirty || (this._errorMeasurement && this._errorMeasurement.awaitingQuery);
return dirty; return dirty;

View File

@ -13,7 +13,8 @@ import {Frustum} from '../../util/primitives/frustum';
import type {Terrain} from '../../render/terrain'; import type {Terrain} from '../../render/terrain';
import type {PointProjection} from '../../symbol/projection'; import type {PointProjection} from '../../symbol/projection';
import type {IReadonlyTransform, ITransform} from '../transform_interface'; import type {IReadonlyTransform, ITransform, TransformConstrainFunction} from '../transform_interface';
import type {TransformOptions} from '../transform_helper';
import type {PaddingOptions} from '../edge_insets'; import type {PaddingOptions} from '../edge_insets';
import type {ProjectionData, ProjectionDataParams} from './projection_data'; import type {ProjectionData, ProjectionDataParams} from './projection_data';
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider'; import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
@ -127,6 +128,9 @@ export class VerticalPerspectiveTransform implements ITransform {
setMaxBounds(bounds?: LngLatBounds): void { setMaxBounds(bounds?: LngLatBounds): void {
this._helper.setMaxBounds(bounds); this._helper.setMaxBounds(bounds);
} }
setConstrain(constrain?: TransformConstrainFunction | null): void {
this._helper.setConstrain(constrain);
}
overrideNearFarZ(nearZ: number, farZ: number): void { overrideNearFarZ(nearZ: number, farZ: number): void {
this._helper.overrideNearFarZ(nearZ, farZ); this._helper.overrideNearFarZ(nearZ, farZ);
} }
@ -218,6 +222,9 @@ export class VerticalPerspectiveTransform implements ITransform {
get renderWorldCopies(): boolean { get renderWorldCopies(): boolean {
return this._helper.renderWorldCopies; return this._helper.renderWorldCopies;
} }
get constrain(): TransformConstrainFunction {
return this._helper.constrain;
}
public get nearZ(): number { public get nearZ(): number {
return this._helper.nearZ; return this._helper.nearZ;
} }
@ -251,12 +258,11 @@ export class VerticalPerspectiveTransform implements ITransform {
private _coveringTilesDetailsProvider: GlobeCoveringTilesDetailsProvider; private _coveringTilesDetailsProvider: GlobeCoveringTilesDetailsProvider;
public constructor() { public constructor(options?: TransformOptions) {
this._helper = new TransformHelper({ this._helper = new TransformHelper({
calcMatrices: () => { this._calcMatrices(); }, calcMatrices: () => { this._calcMatrices(); },
getConstrained: (center, zoom) => { return this.getConstrained(center, zoom); } constrain: (center, zoom) => { return this.defaultConstrain(center, zoom); }
}); }, options);
this._coveringTilesDetailsProvider = new GlobeCoveringTilesDetailsProvider(); this._coveringTilesDetailsProvider = new GlobeCoveringTilesDetailsProvider();
} }
@ -636,7 +642,7 @@ export class VerticalPerspectiveTransform implements ITransform {
return new LngLatBounds(boundsArray); return new LngLatBounds(boundsArray);
} }
getConstrained(lngLat: LngLat, zoom: number): { center: LngLat; zoom: number } { defaultConstrain: TransformConstrainFunction = (lngLat, zoom) => {
// Globe: TODO: respect _lngRange, _latRange // Globe: TODO: respect _lngRange, _latRange
// It is possible to implement exact constrain for globe, but I don't think it is worth the effort. // It is possible to implement exact constrain for globe, but I don't think it is worth the effort.
const constrainedLat = clamp(lngLat.lat, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE); const constrainedLat = clamp(lngLat.lat, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE);
@ -648,7 +654,7 @@ export class VerticalPerspectiveTransform implements ITransform {
), ),
zoom: constrainedZoom zoom: constrainedZoom
}; };
} };
calculateCenterFromCameraLngLatAlt(lngLat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} { calculateCenterFromCameraLngLatAlt(lngLat: LngLatLike, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} {
return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch); return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch);

View File

@ -8,7 +8,7 @@ import {EXTENT} from '../data/extent';
const emptyCallbacks = { const emptyCallbacks = {
calcMatrices: () => {}, calcMatrices: () => {},
getConstrained: (center, zoom) => { return {center, zoom}; }, constrain: (center, zoom) => { return {center, zoom}; },
}; };
describe('TransformHelper', () => { describe('TransformHelper', () => {

View File

@ -9,7 +9,7 @@ import {cameraMercatorCoordinateFromCenterAndRotation} from './projection/mercat
import {EXTENT} from '../data/extent'; import {EXTENT} from '../data/extent';
import type {PaddingOptions} from './edge_insets'; import type {PaddingOptions} from './edge_insets';
import type {IReadonlyTransform, ITransformGetters} from './transform_interface'; import type {IReadonlyTransform, ITransformGetters, TransformConstrainFunction} from './transform_interface';
import type {OverscaledTileID} from '../source/tile_id'; import type {OverscaledTileID} from '../source/tile_id';
import {Bounds} from './bounds'; import {Bounds} from './bounds';
/** /**
@ -50,12 +50,12 @@ export type UnwrappedTileIDType = {
export type TransformHelperCallbacks = { export type TransformHelperCallbacks = {
/** /**
* Get center lngLat and zoom to ensure that * The transform's default getter of center lngLat and zoom to ensure that
* 1) everything beyond the bounds is excluded * 1) everything beyond the bounds is excluded
* 2) a given lngLat is as near the center as possible * 2) a given lngLat is as near the center as possible
* Bounds are those set by maxBounds or North & South "Poles" and, if only 1 globe is displayed, antimeridian. * Bounds are those set by maxBounds or North & South "Poles" and, if only 1 globe is displayed, antimeridian.
*/ */
getConstrained: (center: LngLat, zoom: number) => { center: LngLat; zoom: number }; constrain: TransformConstrainFunction;
/** /**
* Updates the underlying transform's internal matrices. * Updates the underlying transform's internal matrices.
@ -63,6 +63,33 @@ export type TransformHelperCallbacks = {
calcMatrices: () => void; calcMatrices: () => void;
}; };
export type TransformOptions = {
/**
* The minimum zoom level of the map.
*/
minZoom?: number;
/**
* The maximum zoom level of the map.
*/
maxZoom?: number;
/**
* The minimum pitch of the map.
*/
minPitch?: number;
/**
* The maximum pitch of the map.
*/
maxPitch?: number;
/**
* Whether to render multiple copies of the world side by side in the map.
*/
renderWorldCopies?: boolean;
/**
* An override of the transform's constraining function for respecting its longitude and latitude bounds.
*/
constrain?: TransformConstrainFunction | null;
};
function getTileZoom(zoom: number): number { function getTileZoom(zoom: number): number {
return Math.max(0, Math.floor(zoom)); return Math.max(0, Math.floor(zoom));
} }
@ -123,16 +150,20 @@ export class TransformHelper implements ITransformGetters {
_farZ: number; _farZ: number;
_autoCalculateNearFarZ: boolean; _autoCalculateNearFarZ: boolean;
constructor(callbacks: TransformHelperCallbacks, minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) { _constrain: TransformConstrainFunction;
constructor(callbacks: TransformHelperCallbacks, options?: TransformOptions) {
this._callbacks = callbacks; this._callbacks = callbacks;
this._tileSize = 512; // constant this._tileSize = 512; // constant
this._renderWorldCopies = renderWorldCopies === undefined ? true : !!renderWorldCopies; this._renderWorldCopies = options?.renderWorldCopies === undefined ? true : !!options?.renderWorldCopies;
this._minZoom = minZoom || 0; this._minZoom = options?.minZoom || 0;
this._maxZoom = maxZoom || 22; this._maxZoom = options?.maxZoom || 22;
this._minPitch = (minPitch === undefined || minPitch === null) ? 0 : minPitch; this._minPitch = (options?.minPitch === undefined || options?.minPitch === null) ? 0 : options?.minPitch;
this._maxPitch = (maxPitch === undefined || maxPitch === null) ? 60 : maxPitch; this._maxPitch = (options?.maxPitch === undefined || options?.maxPitch === null) ? 60 : options?.maxPitch;
this._constrain = options?.constrain ?? this._callbacks.constrain;
this.setMaxBounds(); this.setMaxBounds();
@ -180,7 +211,7 @@ export class TransformHelper implements ITransformGetters {
this._farZ = thatI.farZ; this._farZ = thatI.farZ;
this._autoCalculateNearFarZ = !forceOverrideZ && thatI.autoCalculateNearFarZ; this._autoCalculateNearFarZ = !forceOverrideZ && thatI.autoCalculateNearFarZ;
if (constrain) { if (constrain) {
this._constrain(); this.constrainInternal();
} }
this._calcMatrices(); this._calcMatrices();
} }
@ -221,14 +252,14 @@ export class TransformHelper implements ITransformGetters {
setMinZoom(zoom: number) { setMinZoom(zoom: number) {
if (this._minZoom === zoom) return; if (this._minZoom === zoom) return;
this._minZoom = zoom; this._minZoom = zoom;
this.setZoom(this.getConstrained(this._center, this.zoom).zoom); this.setZoom(this.constrain(this._center, this.zoom).zoom);
} }
get maxZoom(): number { return this._maxZoom; } get maxZoom(): number { return this._maxZoom; }
setMaxZoom(zoom: number) { setMaxZoom(zoom: number) {
if (this._maxZoom === zoom) return; if (this._maxZoom === zoom) return;
this._maxZoom = zoom; this._maxZoom = zoom;
this.setZoom(this.getConstrained(this._center, this.zoom).zoom); this.setZoom(this.constrain(this._center, this.zoom).zoom);
} }
get minPitch(): number { return this._minPitch; } get minPitch(): number { return this._minPitch; }
@ -255,6 +286,16 @@ export class TransformHelper implements ITransformGetters {
this._renderWorldCopies = renderWorldCopies; this._renderWorldCopies = renderWorldCopies;
} }
get constrain(): TransformConstrainFunction { return this._constrain; }
setConstrain(constrain?: TransformConstrainFunction | null) {
if (!constrain) {
constrain = this._callbacks.constrain;
}
this._constrain = constrain;
this.constrainInternal();
this._calcMatrices();
}
get worldSize(): number { get worldSize(): number {
return this._tileSize * this._scale; return this._tileSize * this._scale;
@ -332,13 +373,13 @@ export class TransformHelper implements ITransformGetters {
get zoom(): number { return this._zoom; } get zoom(): number { return this._zoom; }
setZoom(zoom: number) { setZoom(zoom: number) {
const constrainedZoom = this.getConstrained(this._center, zoom).zoom; const constrainedZoom = this.constrain(this._center, zoom).zoom;
if (this._zoom === constrainedZoom) return; if (this._zoom === constrainedZoom) return;
this._unmodified = false; this._unmodified = false;
this._zoom = constrainedZoom; this._zoom = constrainedZoom;
this._tileZoom = Math.max(0, Math.floor(constrainedZoom)); this._tileZoom = Math.max(0, Math.floor(constrainedZoom));
this._scale = zoomScale(constrainedZoom); this._scale = zoomScale(constrainedZoom);
this._constrain(); this.constrainInternal();
this._calcMatrices(); this._calcMatrices();
} }
@ -347,7 +388,7 @@ export class TransformHelper implements ITransformGetters {
if (center.lat === this._center.lat && center.lng === this._center.lng) return; if (center.lat === this._center.lat && center.lng === this._center.lng) return;
this._unmodified = false; this._unmodified = false;
this._center = center; this._center = center;
this._constrain(); this.constrainInternal();
this._calcMatrices(); this._calcMatrices();
} }
@ -358,7 +399,7 @@ export class TransformHelper implements ITransformGetters {
setElevation(elevation: number) { setElevation(elevation: number) {
if (elevation === this._elevation) return; if (elevation === this._elevation) return;
this._elevation = elevation; this._elevation = elevation;
this._constrain(); this.constrainInternal();
this._calcMatrices(); this._calcMatrices();
} }
@ -422,14 +463,14 @@ export class TransformHelper implements ITransformGetters {
interpolatePadding(start: PaddingOptions, target: PaddingOptions, t: number): void { interpolatePadding(start: PaddingOptions, target: PaddingOptions, t: number): void {
this._unmodified = false; this._unmodified = false;
this._edgeInsets.interpolate(start, target, t); this._edgeInsets.interpolate(start, target, t);
this._constrain(); this.constrainInternal();
this._calcMatrices(); this._calcMatrices();
} }
resize(width: number, height: number, constrain: boolean = true): void { resize(width: number, height: number, constrain: boolean = true): void {
this._width = width; this._width = width;
this._height = height; this._height = height;
if (constrain) this._constrain(); if (constrain) this.constrainInternal();
this._calcMatrices(); this._calcMatrices();
} }
@ -452,17 +493,13 @@ export class TransformHelper implements ITransformGetters {
if (bounds) { if (bounds) {
this._lngRange = [bounds.getWest(), bounds.getEast()]; this._lngRange = [bounds.getWest(), bounds.getEast()];
this._latRange = [bounds.getSouth(), bounds.getNorth()]; this._latRange = [bounds.getSouth(), bounds.getNorth()];
this._constrain(); this.constrainInternal();
} else { } else {
this._lngRange = null; this._lngRange = null;
this._latRange = [-MAX_VALID_LATITUDE, MAX_VALID_LATITUDE]; this._latRange = [-MAX_VALID_LATITUDE, MAX_VALID_LATITUDE];
} }
} }
private getConstrained(lngLat: LngLat, zoom: number): {center: LngLat; zoom: number} {
return this._callbacks.getConstrained(lngLat, zoom);
}
/** /**
* When the map is pitched, some of the 3D features that intersect a query will not intersect * When the map is pitched, some of the 3D features that intersect a query will not intersect
* the query at the surface of the earth. Instead the feature may be closer and only intersect * the query at the surface of the earth. Instead the feature may be closer and only intersect
@ -492,11 +529,11 @@ export class TransformHelper implements ITransformGetters {
* @internal * @internal
* Snaps the transform's center, zoom, etc. into the valid range. * Snaps the transform's center, zoom, etc. into the valid range.
*/ */
private _constrain(): void { private constrainInternal(): void {
if (!this.center || !this._width || !this._height || this._constraining) return; if (!this.center || !this._width || !this._height || this._constraining) return;
this._constraining = true; this._constraining = true;
const unmodified = this._unmodified; const unmodified = this._unmodified;
const {center, zoom} = this.getConstrained(this.center, this.zoom); const {center, zoom} = this.constrain(this.center, this.zoom);
this.setCenter(center); this.setCenter(center);
this.setZoom(zoom); this.setZoom(zoom);
this._unmodified = unmodified; this._unmodified = unmodified;

View File

@ -11,6 +11,18 @@ import type {ProjectionData, ProjectionDataParams} from './projection/projection
import type {CoveringTilesDetailsProvider} from './projection/covering_tiles_details_provider'; import type {CoveringTilesDetailsProvider} from './projection/covering_tiles_details_provider';
import type {Frustum} from '../util/primitives/frustum'; import type {Frustum} from '../util/primitives/frustum';
/**
* The callback defining how the transform constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds.
* @see [Customize the map transform constrain](https://maplibre.org/maplibre-gl-js/docs/examples/customize-the-map-transform-constrain/)
*/
export type TransformConstrainFunction = (
lngLat: LngLat,
zoom: number
) => {
center: LngLat;
zoom: number;
};
export interface ITransformGetters { export interface ITransformGetters {
get tileSize(): number; get tileSize(): number;
@ -83,6 +95,10 @@ export interface ITransformGetters {
get nearZ(): number; get nearZ(): number;
get farZ(): number; get farZ(): number;
get autoCalculateNearFarZ(): boolean; get autoCalculateNearFarZ(): boolean;
/**
* Get center lngLat and zoom to ensure that longitude and latitude bounds are respected and regions beyond the map bounds are not displayed.
*/
get constrain(): TransformConstrainFunction;
} }
/** /**
@ -194,6 +210,12 @@ interface ITransformMutators {
*/ */
setMaxBounds(bounds?: LngLatBounds | null): void; setMaxBounds(bounds?: LngLatBounds | null): void;
/** Sets or clears the callback overriding the transform's default constrain,
* whose responsibility is to respect the longitude and latitude bounds by constraining the viewport's lnglat and zoom.
* @param constrain - A {@link TransformConstrainFunction} callback defining how the viewport should respect the bounds.
*/
setConstrain(constrain?: TransformConstrainFunction | null): void;
/** /**
* @internal * @internal
* Called before rendering to allow the transform implementation * Called before rendering to allow the transform implementation
@ -340,9 +362,10 @@ export interface IReadonlyTransform extends ITransformGetters {
isPointOnMapSurface(p: Point, terrain?: Terrain): boolean; isPointOnMapSurface(p: Point, terrain?: Terrain): boolean;
/** /**
* Get center lngLat and zoom to ensure that longitude and latitude bounds are respected and regions beyond the map bounds are not displayed. * @internal
* The tranform's default callback that ensures that longitude and latitude bounds are respected by the viewport.
*/ */
getConstrained(lngLat: LngLat, zoom: number): {center: LngLat; zoom: number}; defaultConstrain: TransformConstrainFunction;
maxPitchScaleFactor(): number; maxPitchScaleFactor(): number;

View File

@ -18,6 +18,7 @@ import {MercatorCoordinate} from './geo/mercator_coordinate';
import {Evented, type ErrorEvent, Event, type Listener} from './util/evented'; import {Evented, type ErrorEvent, Event, type Listener} from './util/evented';
import {type AddProtocolAction, config} from './util/config'; import {type AddProtocolAction, config} from './util/config';
import {rtlMainThreadPluginFactory} from './source/rtl_text_plugin_main_thread'; import {rtlMainThreadPluginFactory} from './source/rtl_text_plugin_main_thread';
import {setNow, restoreNow, isTimeFrozen} from './util/time_control';
import {WorkerPool} from './util/worker_pool'; import {WorkerPool} from './util/worker_pool';
import {prewarm, clearPrewarmedResources} from './util/global_worker_pool'; import {prewarm, clearPrewarmedResources} from './util/global_worker_pool';
import {AJAXError, type ExpiryData, type GetResourceResponse, type RequestParameters} from './util/ajax'; import {AJAXError, type ExpiryData, type GetResourceResponse, type RequestParameters} from './util/ajax';
@ -52,6 +53,7 @@ import type {DistributiveKeys, DistributiveOmit, GeoJSONFeature, MapGeoJSONFeatu
import type {Handler, HandlerResult} from './ui/handler_manager'; import type {Handler, HandlerResult} from './ui/handler_manager';
import type {Complete, RequireAtLeastOne, Subscription} from './util/util'; import type {Complete, RequireAtLeastOne, Subscription} from './util/util';
import type {CalculateTileZoomFunction, CoveringTilesOptions} from './geo/projection/covering_tiles'; import type {CalculateTileZoomFunction, CoveringTilesOptions} from './geo/projection/covering_tiles';
import type {TransformConstrainFunction} from './geo/transform_interface';
import type {StyleImage, StyleImageData, StyleImageInterface, StyleImageMetadata, TextFit} from './style/style_image'; import type {StyleImage, StyleImageData, StyleImageInterface, StyleImageMetadata, TextFit} from './style/style_image';
import type {StyleLayer} from './style/style_layer'; import type {StyleLayer} from './style/style_layer';
import type {Tile} from './source/tile'; import type {Tile} from './source/tile';
@ -70,6 +72,7 @@ import type {GlyphPosition, GlyphPositions} from './render/glyph_atlas';
import type {ImageAtlas} from './render/image_atlas'; import type {ImageAtlas} from './render/image_atlas';
import type {StyleGlyph} from './style/style_glyph'; import type {StyleGlyph} from './style/style_glyph';
import type {FeatureIndex} from './data/feature_index'; import type {FeatureIndex} from './data/feature_index';
import type {DashEntry} from './render/line_atlas';
const version = packageJSON.version; const version = packageJSON.version;
export type * from '@maplibre/maplibre-gl-style-spec'; export type * from '@maplibre/maplibre-gl-style-spec';
@ -241,6 +244,7 @@ export {
type Handler, type Handler,
type RequireAtLeastOne, type RequireAtLeastOne,
type CameraUpdateTransformFunction, type CameraUpdateTransformFunction,
type TransformConstrainFunction,
type CustomRenderMethod, type CustomRenderMethod,
type CalculateTileZoomFunction, type CalculateTileZoomFunction,
type MapSourceDataType, type MapSourceDataType,
@ -351,6 +355,7 @@ export {
type ErrorEvent, type ErrorEvent,
type GeoJSONFeature, type GeoJSONFeature,
type CoveringTilesOptions, type CoveringTilesOptions,
type DashEntry,
setRTLTextPlugin, setRTLTextPlugin,
getRTLTextPluginStatus, getRTLTextPluginStatus,
prewarm, prewarm,
@ -366,5 +371,8 @@ export {
removeProtocol, removeProtocol,
addSourceType, addSourceType,
importScriptInWorkers, importScriptInWorkers,
createTileMesh createTileMesh,
setNow,
restoreNow,
isTimeFrozen
}; };

View File

@ -23,7 +23,7 @@ vi.mock('../symbol/projection');
describe('drawCustom', () => { describe('drawCustom', () => {
test('should return custom render method inputs', () => { test('should return custom render method inputs', () => {
// same transform setup as in transform.test.ts 'creates a transform', so matrices of transform should be the same // same transform setup as in transform.test.ts 'creates a transform', so matrices of transform should be the same
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(500, 500); transform.resize(500, 500);
transform.setMinPitch(10); transform.setMinPitch(10);
transform.setMaxPitch(10); transform.setMaxPitch(10);

View File

@ -5,7 +5,8 @@ import {
lineUniformValues, lineUniformValues,
linePatternUniformValues, linePatternUniformValues,
lineSDFUniformValues, lineSDFUniformValues,
lineGradientUniformValues lineGradientUniformValues,
lineGradientSDFUniformValues
} from './program/line_program'; } from './program/line_program';
import type {Painter, RenderOptions} from './painter'; import type {Painter, RenderOptions} from './painter';
@ -13,9 +14,129 @@ import type {SourceCache} from '../source/source_cache';
import type {LineStyleLayer} from '../style/style_layer/line_style_layer'; import type {LineStyleLayer} from '../style/style_layer/line_style_layer';
import type {LineBucket} from '../data/bucket/line_bucket'; import type {LineBucket} from '../data/bucket/line_bucket';
import type {OverscaledTileID} from '../source/tile_id'; import type {OverscaledTileID} from '../source/tile_id';
import type {Tile} from '../source/tile';
import type {Context} from '../gl/context';
import type {ProgramConfiguration} from '../data/program_configuration';
import {clamp, nextPowerOfTwo} from '../util/util'; import {clamp, nextPowerOfTwo} from '../util/util';
import {renderColorRamp} from '../util/color_ramp'; import {renderColorRamp} from '../util/color_ramp';
import {EXTENT} from '../data/extent'; import {EXTENT} from '../data/extent';
import type {RGBAImage} from '../util/image';
type GradientTexture = {
texture?: Texture;
gradient?: RGBAImage;
version?: number;
};
function updateGradientTexture(
painter: Painter,
sourceCache: SourceCache,
context: Context,
gl: WebGLRenderingContext,
layer: LineStyleLayer,
bucket: LineBucket,
coord: OverscaledTileID,
layerGradient: GradientTexture
): Texture {
let textureResolution = 256;
if (layer.stepInterpolant) {
const sourceMaxZoom = sourceCache.getSource().maxzoom;
const potentialOverzoom = coord.canonical.z === sourceMaxZoom ?
Math.ceil(1 << (painter.transform.maxZoom - coord.canonical.z)) : 1;
const lineLength = bucket.maxLineLength / EXTENT;
// Logical pixel tile size is 512px, and 1024px right before current zoom + 1
const maxTilePixelSize = 1024;
// Maximum possible texture coverage heuristic, bound by hardware max texture size
const maxTextureCoverage = lineLength * maxTilePixelSize * potentialOverzoom;
textureResolution = clamp(nextPowerOfTwo(maxTextureCoverage), 256, context.maxTextureSize);
}
layerGradient.gradient = renderColorRamp({
expression: layer.gradientExpression(),
evaluationKey: 'lineProgress',
resolution: textureResolution,
image: layerGradient.gradient || undefined,
clips: bucket.lineClipsArray
});
if (layerGradient.texture) {
layerGradient.texture.update(layerGradient.gradient);
} else {
layerGradient.texture = new Texture(context, layerGradient.gradient, gl.RGBA);
}
layerGradient.version = layer.gradientVersion;
return layerGradient.texture;
}
function bindImagePatternTextures(
context: Context,
gl: WebGLRenderingContext,
tile: Tile,
programConfiguration: ProgramConfiguration,
crossfade: ReturnType<LineStyleLayer['getCrossfadeParameters']>
) {
context.activeTexture.set(gl.TEXTURE0);
tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
programConfiguration.updatePaintBuffers(crossfade);
}
function bindDasharrayTextures(
painter: Painter,
context: Context,
gl: WebGLRenderingContext,
programConfiguration: ProgramConfiguration,
programChanged: boolean,
crossfade: ReturnType<LineStyleLayer['getCrossfadeParameters']>
) {
if (programChanged || painter.lineAtlas.dirty) {
context.activeTexture.set(gl.TEXTURE0);
painter.lineAtlas.bind(context);
}
programConfiguration.updatePaintBuffers(crossfade);
}
function bindGradientTextures(
painter: Painter,
sourceCache: SourceCache,
context: Context,
gl: WebGLRenderingContext,
layer: LineStyleLayer,
bucket: LineBucket,
coord: OverscaledTileID
) {
const layerGradient = bucket.gradients[layer.id];
let gradientTexture = layerGradient.texture;
if (layer.gradientVersion !== layerGradient.version) {
gradientTexture = updateGradientTexture(painter, sourceCache, context, gl, layer, bucket, coord, layerGradient);
}
context.activeTexture.set(gl.TEXTURE0);
gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE);
}
function bindGradientAndDashTextures(
painter: Painter,
sourceCache: SourceCache,
context: Context,
gl: WebGLRenderingContext,
layer: LineStyleLayer,
bucket: LineBucket,
coord: OverscaledTileID,
programConfiguration: ProgramConfiguration,
crossfade: ReturnType<LineStyleLayer['getCrossfadeParameters']>
) {
// Bind gradient texture to TEXTURE0
const layerGradient = bucket.gradients[layer.id];
let gradientTexture = layerGradient.texture;
if (layer.gradientVersion !== layerGradient.version) {
gradientTexture = updateGradientTexture(painter, sourceCache, context, gl, layer, bucket, coord, layerGradient);
}
context.activeTexture.set(gl.TEXTURE0);
gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE);
// Bind dash atlas to TEXTURE1
context.activeTexture.set(gl.TEXTURE1);
painter.lineAtlas.bind(context);
programConfiguration.updatePaintBuffers(crossfade);
}
export function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array<OverscaledTileID>, renderOptions: RenderOptions) { export function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array<OverscaledTileID>, renderOptions: RenderOptions) {
if (painter.renderPass !== 'translucent') return; if (painter.renderPass !== 'translucent') return;
@ -28,18 +149,21 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line
const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly);
const colorMode = painter.colorModeForRenderPass(); const colorMode = painter.colorModeForRenderPass();
const dasharray = layer.paint.get('line-dasharray'); const dasharrayProperty = layer.paint.get('line-dasharray');
const dasharray = dasharrayProperty.constantOr(1 as any);
const patternProperty = layer.paint.get('line-pattern'); const patternProperty = layer.paint.get('line-pattern');
const image = patternProperty.constantOr(1 as any); const image = patternProperty.constantOr(1 as any);
const gradient = layer.paint.get('line-gradient'); const gradient = layer.paint.get('line-gradient');
const crossfade = layer.getCrossfadeParameters(); const crossfade = layer.getCrossfadeParameters();
const programId = let programId: string;
image ? 'linePattern' : if (image) programId = 'linePattern';
dasharray ? 'lineSDF' : else if (dasharray && gradient) programId = 'lineGradientSDF';
gradient ? 'lineGradient' : 'line'; else if (dasharray) programId = 'lineSDF';
else if (gradient) programId = 'lineGradient';
else programId = 'line';
const context = painter.context; const context = painter.context;
const gl = context.gl; const gl = context.gl;
@ -62,11 +186,19 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line
const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord);
const constantPattern = patternProperty.constantOr(null); const constantPattern = patternProperty.constantOr(null);
const constantDasharray = dasharrayProperty && dasharrayProperty.constantOr(null);
if (constantPattern && tile.imageAtlas) { if (constantPattern && tile.imageAtlas) {
const atlas = tile.imageAtlas; const atlas = tile.imageAtlas;
const posTo = atlas.patternPositions[constantPattern.to.toString()]; const posTo = atlas.patternPositions[constantPattern.to.toString()];
const posFrom = atlas.patternPositions[constantPattern.from.toString()]; const posFrom = atlas.patternPositions[constantPattern.from.toString()];
if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
} else if (constantDasharray) {
const round = layer.layout.get('line-cap') === 'round';
const dashTo = painter.lineAtlas.getDash(constantDasharray.to, round);
const dashFrom = painter.lineAtlas.getDash(constantDasharray.from, round);
programConfiguration.setConstantDashPositions(dashTo, dashFrom);
} }
const projectionData = transform.getProjectionData({ const projectionData = transform.getProjectionData({
@ -77,51 +209,21 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line
const pixelRatio = transform.getPixelScale(); const pixelRatio = transform.getPixelScale();
const uniformValues = image ? linePatternUniformValues(painter, tile, layer, pixelRatio, crossfade) : let uniformValues;
dasharray ? lineSDFUniformValues(painter, tile, layer, pixelRatio, dasharray, crossfade) :
gradient ? lineGradientUniformValues(painter, tile, layer, pixelRatio, bucket.lineClipsArray.length) :
lineUniformValues(painter, tile, layer, pixelRatio);
if (image) { if (image) {
context.activeTexture.set(gl.TEXTURE0); uniformValues = linePatternUniformValues(painter, tile, layer, pixelRatio, crossfade);
tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); bindImagePatternTextures(context, gl, tile, programConfiguration, crossfade);
programConfiguration.updatePaintBuffers(crossfade); } else if (dasharray && gradient) {
} else if (dasharray && (programChanged || painter.lineAtlas.dirty)) { uniformValues = lineGradientSDFUniformValues(painter, tile, layer, pixelRatio, crossfade, bucket.lineClipsArray.length);
context.activeTexture.set(gl.TEXTURE0); bindGradientAndDashTextures(painter, sourceCache, context, gl, layer, bucket, coord, programConfiguration, crossfade);
painter.lineAtlas.bind(context); } else if (dasharray) {
uniformValues = lineSDFUniformValues(painter, tile, layer, pixelRatio, crossfade);
bindDasharrayTextures(painter, context, gl, programConfiguration, programChanged, crossfade);
} else if (gradient) { } else if (gradient) {
const layerGradient = bucket.gradients[layer.id]; uniformValues = lineGradientUniformValues(painter, tile, layer, pixelRatio, bucket.lineClipsArray.length);
let gradientTexture = layerGradient.texture; bindGradientTextures(painter, sourceCache, context, gl, layer, bucket, coord);
if (layer.gradientVersion !== layerGradient.version) { } else {
let textureResolution = 256; uniformValues = lineUniformValues(painter, tile, layer, pixelRatio);
if (layer.stepInterpolant) {
const sourceMaxZoom = sourceCache.getSource().maxzoom;
const potentialOverzoom = coord.canonical.z === sourceMaxZoom ?
Math.ceil(1 << (painter.transform.maxZoom - coord.canonical.z)) : 1;
const lineLength = bucket.maxLineLength / EXTENT;
// Logical pixel tile size is 512px, and 1024px right before current zoom + 1
const maxTilePixelSize = 1024;
// Maximum possible texture coverage heuristic, bound by hardware max texture size
const maxTextureCoverage = lineLength * maxTilePixelSize * potentialOverzoom;
textureResolution = clamp(nextPowerOfTwo(maxTextureCoverage), 256, context.maxTextureSize);
}
layerGradient.gradient = renderColorRamp({
expression: layer.gradientExpression(),
evaluationKey: 'lineProgress',
resolution: textureResolution,
image: layerGradient.gradient || undefined,
clips: bucket.lineClipsArray
});
if (layerGradient.texture) {
layerGradient.texture.update(layerGradient.gradient);
} else {
layerGradient.texture = new Texture(context, layerGradient.gradient, gl.RGBA);
}
layerGradient.version = layer.gradientVersion;
gradientTexture = layerGradient.texture;
}
context.activeTexture.set(gl.TEXTURE0);
gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE);
} }
const stencil = painter.stencilModeForClipping(coord); const stencil = painter.stencilModeForClipping(coord);

View File

@ -1,22 +1,33 @@
import {clamp} from '../util/util'; import {clamp} from '../util/util';
import {ImageSource} from '../source/image_source'; import {ImageSource} from '../source/image_source';
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {StencilMode} from '../gl/stencil_mode'; import {StencilMode} from '../gl/stencil_mode';
import {DepthMode} from '../gl/depth_mode'; import {DepthMode} from '../gl/depth_mode';
import {CullFaceMode} from '../gl/cull_face_mode'; import {CullFaceMode} from '../gl/cull_face_mode';
import {rasterUniformValues} from './program/raster_program'; import {rasterUniformValues} from './program/raster_program';
import {EXTENT} from '../data/extent'; import {EXTENT} from '../data/extent';
import {coveringZoomLevel} from '../geo/projection/covering_tiles'; import {FadingDirections} from '../source/tile';
import Point from '@mapbox/point-geometry'; import Point from '@mapbox/point-geometry';
import type {Painter, RenderOptions} from './painter'; import type {Painter, RenderOptions} from './painter';
import type {SourceCache} from '../source/source_cache'; import type {SourceCache} from '../source/source_cache';
import type {RasterStyleLayer} from '../style/style_layer/raster_style_layer'; import type {RasterStyleLayer} from '../style/style_layer/raster_style_layer';
import type {OverscaledTileID} from '../source/tile_id'; import type {OverscaledTileID} from '../source/tile_id';
import type {IReadonlyTransform} from '../geo/transform_interface';
import type {Tile} from '../source/tile'; import type {Tile} from '../source/tile';
import type {Terrain} from './terrain';
type FadeProperties = {
parentTile: Tile;
parentScaleBy: number;
parentTopLeft: [number, number];
fadeValues: FadeValues;
};
type FadeValues = {
tileOpacity: number;
parentTileOpacity?: number;
fadeMix: {opacity: number; mix: number};
};
const cornerCoords = [ const cornerCoords = [
new Point(0, 0), new Point(0, 0),
@ -83,37 +94,32 @@ function drawTiles(
const colorMode = painter.colorModeForRenderPass(); const colorMode = painter.colorModeForRenderPass();
const align = !painter.options.moving; const align = !painter.options.moving;
const rasterOpacity = layer.paint.get('raster-opacity');
const rasterResampling = layer.paint.get('raster-resampling');
const fadeDuration = layer.paint.get('raster-fade-duration');
const isTerrain = !!painter.style.map.terrain;
// Draw all tiles // Draw all tiles
for (const coord of coords) { for (const coord of coords) {
// Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers // Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers
// Use gl.LESS to prevent double drawing in areas where tiles overlap. // Use gl.LESS to prevent double drawing in areas where tiles overlap.
const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ, const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ,
layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS); rasterOpacity === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS);
const tile = sourceCache.getTile(coord); const tile = sourceCache.getTile(coord);
const textureFilter = rasterResampling === 'nearest' ? gl.NEAREST : gl.LINEAR;
tile.registerFadeDuration(layer.paint.get('raster-fade-duration')); // create and bind first texture
const parentTile = sourceCache.findLoadedParent(coord, 0);
const siblingTile = sourceCache.findLoadedSibling(coord);
// Prefer parent tile if present
const fadeTileReference = parentTile || siblingTile || null;
const fade = getFadeValues(tile, fadeTileReference, sourceCache, layer, painter.transform, painter.style.map.terrain);
let parentScaleBy, parentTL;
const textureFilter = layer.paint.get('raster-resampling') === 'nearest' ? gl.NEAREST : gl.LINEAR;
context.activeTexture.set(gl.TEXTURE0); context.activeTexture.set(gl.TEXTURE0);
tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
// create second texture - use either the current tile or fade tile to bind second texture below
context.activeTexture.set(gl.TEXTURE1); context.activeTexture.set(gl.TEXTURE1);
const {parentTile, parentScaleBy, parentTopLeft, fadeValues} = getFadeProperties(tile, sourceCache, fadeDuration, isTerrain);
tile.fadeOpacity = fadeValues.tileOpacity;
if (parentTile) { if (parentTile) {
parentTile.fadeOpacity = fadeValues.parentTileOpacity;
parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ);
parentTL = [tile.tileID.canonical.x * parentScaleBy % 1, tile.tileID.canonical.y * parentScaleBy % 1];
} else { } else {
tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
} }
@ -127,10 +133,9 @@ function drawTiles(
const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord);
const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align, applyGlobeMatrix: !isRenderingToTexture, applyTerrainMatrix: true}); const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align, applyGlobeMatrix: !isRenderingToTexture, applyTerrainMatrix: true});
const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners); const uniformValues = rasterUniformValues(parentTopLeft, parentScaleBy, fadeValues.fadeMix, layer, corners);
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster'); const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster');
const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled; const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, flipCullfaceMode ? CullFaceMode.frontCCW : CullFaceMode.backCCW, program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, flipCullfaceMode ? CullFaceMode.frontCCW : CullFaceMode.backCCW,
@ -139,46 +144,79 @@ function drawTiles(
} }
} }
function getFadeValues(tile: Tile, parentTile: Tile, sourceCache: SourceCache, layer: RasterStyleLayer, transform: IReadonlyTransform, terrain: Terrain) { /**
const fadeDuration = layer.paint.get('raster-fade-duration'); * Get fade properties for current tile - either cross-fading or self-fading properties.
*/
function getFadeProperties(tile: Tile, sourceCache: SourceCache, fadeDuration: number, isTerrain: boolean): FadeProperties {
const defaults: FadeProperties = {
parentTile: null,
parentScaleBy: 1,
parentTopLeft: [0, 0],
fadeValues: {tileOpacity: 1, parentTileOpacity: 1, fadeMix: {opacity: 1, mix: 0}}
};
if (!terrain && fadeDuration > 0) { if (fadeDuration === 0 || isTerrain) return defaults;
const now = browser.now();
const sinceTile = (now - tile.timeAdded) / fadeDuration;
const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1;
const source = sourceCache.getSource(); // cross-fade with parent first if available
const idealZ = coveringZoomLevel(transform, { if (tile.fadingParentID) {
tileSize: source.tileSize, const parentTile = sourceCache._getLoadedTile(tile.fadingParentID);
roundZoom: source.roundZoom if (!parentTile) return defaults;
});
// if no parent or parent is older, fade in; if parent is younger, fade out const parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ);
const fadeIn = !parentTile || Math.abs(parentTile.tileID.overscaledZ - idealZ) > Math.abs(tile.tileID.overscaledZ - idealZ); const parentTopLeft: [number, number] = [
(tile.tileID.canonical.x * parentScaleBy) % 1,
(tile.tileID.canonical.y * parentScaleBy) % 1
];
const childOpacity = (fadeIn && tile.refreshedUponExpiration) ? 1 : clamp(fadeIn ? sinceTile : 1 - sinceParent, 0, 1); const fadeValues = getCrossFadeValues(tile, parentTile, fadeDuration);
return {parentTile, parentScaleBy, parentTopLeft, fadeValues};
// we don't crossfade tiles that were just refreshed upon expiring:
// once they're old enough to pass the crossfading threshold
// (fadeDuration), unset the `refreshedUponExpiration` flag so we don't
// incorrectly fail to crossfade them when zooming
if (tile.refreshedUponExpiration && sinceTile >= 1) tile.refreshedUponExpiration = false;
if (parentTile) {
return {
opacity: 1,
mix: 1 - childOpacity
};
} else {
return {
opacity: childOpacity,
mix: 0
};
}
} else {
return {
opacity: 1,
mix: 0
};
} }
// self-fade for edge tiles
if (tile.selfFading) {
const fadeValues = getSelfFadeValues(tile, fadeDuration);
return {parentTile: null, parentScaleBy: 1, parentTopLeft: [0, 0], fadeValues};
}
return defaults;
}
/**
* Cross-fade values for a base tile with a parent tile (for zooming in/out)
*/
function getCrossFadeValues(tile: Tile, parentTile: Tile, fadeDuration: number): FadeValues {
const currentTime = now();
const timeSinceTile = (currentTime - tile.timeAdded) / fadeDuration;
const timeSinceParent = (currentTime - parentTile.timeAdded) / fadeDuration;
// get fading opacity based on current fade direction
const doFadeIn = (tile.fadingDirection === FadingDirections.Incoming);
const opacity1 = clamp(timeSinceTile, 0, 1);
const opacity2 = clamp(1 - timeSinceParent, 0, 1);
const tileOpacity = doFadeIn ? opacity1 : opacity2;
const parentTileOpacity = doFadeIn ? opacity2 : opacity1;
const fadeMix = {
opacity: 1,
mix: 1 - tileOpacity
};
return {tileOpacity, parentTileOpacity, fadeMix};
}
/**
* Simple fade-in values for tile without a parent (i.e. edge tiles)
*/
function getSelfFadeValues(tile: Tile, fadeDuration: number): FadeValues {
const currentTime = now();
const timeSinceTile = (currentTime - tile.timeAdded) / fadeDuration;
const tileOpacity = clamp(timeSinceTile, 0, 1);
const fadeMix = {
opacity: tileOpacity,
mix: 0
};
return {tileOpacity, fadeMix};
} }

View File

@ -5,7 +5,7 @@ import type {Context} from '../gl/context';
/** /**
* A dash entry * A dash entry
*/ */
type DashEntry = { export type DashEntry = {
y: number; y: number;
height: number; height: number;
width: number; width: number;
@ -180,8 +180,8 @@ export class LineAtlas {
} }
const dashEntry = { const dashEntry = {
y: (this.nextRow + n + 0.5) / this.height, y: this.nextRow + n,
height: 2 * n / this.height, height: 2 * n,
width: length width: length
}; };

View File

@ -8,7 +8,7 @@ const getStubMap = () => new StubMap() as any;
test('Render must not fail with incompletely loaded style', () => { test('Render must not fail with incompletely loaded style', () => {
const gl = document.createElement('canvas').getContext('webgl'); const gl = document.createElement('canvas').getContext('webgl');
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
const painter = new Painter(gl, transform); const painter = new Painter(gl, transform);
const map = getStubMap(); const map = getStubMap();
const style = new Style(map); const style = new Style(map);

View File

@ -1,4 +1,4 @@
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {mat4} from 'gl-matrix'; import {mat4} from 'gl-matrix';
import {SourceCache} from '../source/source_cache'; import {SourceCache} from '../source/source_cache';
import {EXTENT} from '../data/extent'; import {EXTENT} from '../data/extent';
@ -484,7 +484,7 @@ export class Painter {
this.imageManager = style.imageManager; this.imageManager = style.imageManager;
this.glyphManager = style.glyphManager; this.glyphManager = style.glyphManager;
this.symbolFadeChange = style.placement.symbolFadeChange(browser.now()); this.symbolFadeChange = style.placement.symbolFadeChange(now());
this.imageManager.beginFrame(); this.imageManager.beginFrame();

View File

@ -6,7 +6,6 @@ import type {Context} from '../../gl/context';
import type {UniformValues, UniformLocations} from '../uniform_binding'; import type {UniformValues, UniformLocations} from '../uniform_binding';
import type {IReadonlyTransform} from '../../geo/transform_interface'; import type {IReadonlyTransform} from '../../geo/transform_interface';
import type {Tile} from '../../source/tile'; import type {Tile} from '../../source/tile';
import type {CrossFaded} from '../../style/properties';
import type {LineStyleLayer} from '../../style/style_layer/line_style_layer'; import type {LineStyleLayer} from '../../style/style_layer/line_style_layer';
import type {Painter} from '../painter'; import type {Painter} from '../painter';
import type {CrossfadeParameters} from '../../style/evaluation_parameters'; import type {CrossfadeParameters} from '../../style/evaluation_parameters';
@ -43,13 +42,29 @@ export type LineSDFUniformsType = {
'u_ratio': Uniform1f; 'u_ratio': Uniform1f;
'u_device_pixel_ratio': Uniform1f; 'u_device_pixel_ratio': Uniform1f;
'u_units_to_pixels': Uniform2f; 'u_units_to_pixels': Uniform2f;
'u_patternscale_a': Uniform2f; 'u_tileratio': Uniform1f;
'u_patternscale_b': Uniform2f; 'u_crossfade_from': Uniform1f;
'u_sdfgamma': Uniform1f; 'u_crossfade_to': Uniform1f;
'u_image': Uniform1i; 'u_image': Uniform1i;
'u_tex_y_a': Uniform1f;
'u_tex_y_b': Uniform1f;
'u_mix': Uniform1f; 'u_mix': Uniform1f;
'u_lineatlas_width': Uniform1f;
'u_lineatlas_height': Uniform1f;
};
export type LineGradientSDFUniformsType = {
'u_translation': Uniform2f;
'u_ratio': Uniform1f;
'u_device_pixel_ratio': Uniform1f;
'u_units_to_pixels': Uniform2f;
'u_image': Uniform1i;
'u_image_height': Uniform1f;
'u_tileratio': Uniform1f;
'u_crossfade_from': Uniform1f;
'u_crossfade_to': Uniform1f;
'u_image_dash': Uniform1i;
'u_mix': Uniform1f;
'u_lineatlas_width': Uniform1f;
'u_lineatlas_height': Uniform1f;
}; };
const lineUniforms = (context: Context, locations: UniformLocations): LineUniformsType => ({ const lineUniforms = (context: Context, locations: UniformLocations): LineUniformsType => ({
@ -84,13 +99,29 @@ const lineSDFUniforms = (context: Context, locations: UniformLocations): LineSDF
'u_ratio': new Uniform1f(context, locations.u_ratio), 'u_ratio': new Uniform1f(context, locations.u_ratio),
'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio),
'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels),
'u_patternscale_a': new Uniform2f(context, locations.u_patternscale_a),
'u_patternscale_b': new Uniform2f(context, locations.u_patternscale_b),
'u_sdfgamma': new Uniform1f(context, locations.u_sdfgamma),
'u_image': new Uniform1i(context, locations.u_image), 'u_image': new Uniform1i(context, locations.u_image),
'u_tex_y_a': new Uniform1f(context, locations.u_tex_y_a), 'u_mix': new Uniform1f(context, locations.u_mix),
'u_tex_y_b': new Uniform1f(context, locations.u_tex_y_b), 'u_tileratio': new Uniform1f(context, locations.u_tileratio),
'u_mix': new Uniform1f(context, locations.u_mix) 'u_crossfade_from': new Uniform1f(context, locations.u_crossfade_from),
'u_crossfade_to': new Uniform1f(context, locations.u_crossfade_to),
'u_lineatlas_width': new Uniform1f(context, locations.u_lineatlas_width),
'u_lineatlas_height': new Uniform1f(context, locations.u_lineatlas_height)
});
const lineGradientSDFUniforms = (context: Context, locations: UniformLocations): LineGradientSDFUniformsType => ({
'u_translation': new Uniform2f(context, locations.u_translation),
'u_ratio': new Uniform1f(context, locations.u_ratio),
'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio),
'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels),
'u_image': new Uniform1i(context, locations.u_image),
'u_image_height': new Uniform1f(context, locations.u_image_height),
'u_tileratio': new Uniform1f(context, locations.u_tileratio),
'u_crossfade_from': new Uniform1f(context, locations.u_crossfade_from),
'u_crossfade_to': new Uniform1f(context, locations.u_crossfade_to),
'u_image_dash': new Uniform1i(context, locations.u_image_dash),
'u_mix': new Uniform1f(context, locations.u_mix),
'u_lineatlas_width': new Uniform1f(context, locations.u_lineatlas_width),
'u_lineatlas_height': new Uniform1f(context, locations.u_lineatlas_height)
}); });
const lineUniformValues = ( const lineUniformValues = (
@ -155,29 +186,43 @@ const lineSDFUniformValues = (
tile: Tile, tile: Tile,
layer: LineStyleLayer, layer: LineStyleLayer,
ratioScale: number, ratioScale: number,
dasharray: CrossFaded<Array<number>>,
crossfade: CrossfadeParameters, crossfade: CrossfadeParameters,
): UniformValues<LineSDFUniformsType> => { ): UniformValues<LineSDFUniformsType> => {
const transform = painter.transform; const transform = painter.transform;
const lineAtlas = painter.lineAtlas;
const tileRatio = calculateTileRatio(tile, transform); const tileRatio = calculateTileRatio(tile, transform);
const round = layer.layout.get('line-cap') === 'round'; return extend(lineUniformValues(painter, tile, layer, ratioScale), {
'u_tileratio': tileRatio,
'u_crossfade_from': crossfade.fromScale,
'u_crossfade_to': crossfade.toScale,
'u_image': 0,
'u_mix': crossfade.t,
'u_lineatlas_width': painter.lineAtlas.width,
'u_lineatlas_height': painter.lineAtlas.height,
});
};
const posA = lineAtlas.getDash(dasharray.from, round); const lineGradientSDFUniformValues = (
const posB = lineAtlas.getDash(dasharray.to, round); painter: Painter,
tile: Tile,
const widthA = posA.width * crossfade.fromScale; layer: LineStyleLayer,
const widthB = posB.width * crossfade.toScale; ratioScale: number,
crossfade: CrossfadeParameters,
imageHeight: number,
): UniformValues<LineGradientSDFUniformsType> => {
const transform = painter.transform;
const tileRatio = calculateTileRatio(tile, transform);
return extend(lineUniformValues(painter, tile, layer, ratioScale), { return extend(lineUniformValues(painter, tile, layer, ratioScale), {
'u_patternscale_a': [tileRatio / widthA, -posA.height / 2],
'u_patternscale_b': [tileRatio / widthB, -posB.height / 2],
'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * painter.pixelRatio) / 2,
'u_image': 0, 'u_image': 0,
'u_tex_y_a': posA.y, 'u_image_height': imageHeight,
'u_tex_y_b': posB.y, 'u_tileratio': tileRatio,
'u_mix': crossfade.t 'u_crossfade_from': crossfade.fromScale,
'u_crossfade_to': crossfade.toScale,
'u_image_dash': 1,
'u_mix': crossfade.t,
'u_lineatlas_width': painter.lineAtlas.width,
'u_lineatlas_height': painter.lineAtlas.height,
}); });
}; };
@ -200,8 +245,10 @@ export {
lineGradientUniforms, lineGradientUniforms,
linePatternUniforms, linePatternUniforms,
lineSDFUniforms, lineSDFUniforms,
lineGradientSDFUniforms,
lineUniformValues, lineUniformValues,
lineGradientUniformValues, lineGradientUniformValues,
linePatternUniformValues, linePatternUniformValues,
lineSDFUniformValues lineSDFUniformValues,
lineGradientSDFUniformValues
}; };

View File

@ -6,7 +6,7 @@ import {debugUniforms} from './debug_program';
import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program'; import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program';
import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program'; import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program';
import {colorReliefUniforms} from './color_relief_program'; import {colorReliefUniforms} from './color_relief_program';
import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms} from './line_program'; import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms, lineGradientSDFUniforms} from './line_program';
import {rasterUniforms} from './raster_program'; import {rasterUniforms} from './raster_program';
import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program'; import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program';
import {backgroundUniforms, backgroundPatternUniforms} from './background_program'; import {backgroundUniforms, backgroundPatternUniforms} from './background_program';
@ -39,6 +39,7 @@ export const programUniforms = {
lineGradient: lineGradientUniforms, lineGradient: lineGradientUniforms,
linePattern: linePatternUniforms, linePattern: linePatternUniforms,
lineSDF: lineSDFUniforms, lineSDF: lineSDFUniforms,
lineGradientSDF: lineGradientSDFUniforms,
raster: rasterUniforms, raster: rasterUniforms,
symbolIcon: symbolIconUniforms, symbolIcon: symbolIconUniforms,
symbolSDF: symbolSDFUniforms, symbolSDF: symbolSDFUniforms,

View File

@ -1,7 +1,7 @@
uniform lowp float u_device_pixel_ratio; uniform lowp float u_device_pixel_ratio;
uniform lowp float u_lineatlas_width;
uniform sampler2D u_image; uniform sampler2D u_image;
uniform float u_sdfgamma;
uniform float u_mix; uniform float u_mix;
in vec2 v_normal; in vec2 v_normal;
@ -18,6 +18,8 @@ in float v_depth;
#pragma mapbox: define lowp float opacity #pragma mapbox: define lowp float opacity
#pragma mapbox: define mediump float width #pragma mapbox: define mediump float width
#pragma mapbox: define lowp float floorwidth #pragma mapbox: define lowp float floorwidth
#pragma mapbox: define mediump vec4 dasharray_from
#pragma mapbox: define mediump vec4 dasharray_to
void main() { void main() {
#pragma mapbox: initialize highp vec4 color #pragma mapbox: initialize highp vec4 color
@ -25,6 +27,8 @@ void main() {
#pragma mapbox: initialize lowp float opacity #pragma mapbox: initialize lowp float opacity
#pragma mapbox: initialize mediump float width #pragma mapbox: initialize mediump float width
#pragma mapbox: initialize lowp float floorwidth #pragma mapbox: initialize lowp float floorwidth
#pragma mapbox: initialize mediump vec4 dasharray_from
#pragma mapbox: initialize mediump vec4 dasharray_to
// Calculate the distance of the pixel from the line in pixels. // Calculate the distance of the pixel from the line in pixels.
float dist = length(v_normal) * v_width2.s; float dist = length(v_normal) * v_width2.s;
@ -38,7 +42,8 @@ void main() {
float sdfdist_a = texture(u_image, v_tex_a).a; float sdfdist_a = texture(u_image, v_tex_a).a;
float sdfdist_b = texture(u_image, v_tex_b).a; float sdfdist_b = texture(u_image, v_tex_b).a;
float sdfdist = mix(sdfdist_a, sdfdist_b, u_mix); float sdfdist = mix(sdfdist_a, sdfdist_b, u_mix);
alpha *= smoothstep(0.5 - u_sdfgamma / floorwidth, 0.5 + u_sdfgamma / floorwidth, sdfdist); float sdfgamma = (u_lineatlas_width / 256.0 / u_device_pixel_ratio) / min(dasharray_from.w, dasharray_to.w);
alpha *= smoothstep(0.5 - sdfgamma / floorwidth, 0.5 + sdfgamma / floorwidth, sdfdist);
fragColor = color * (alpha * opacity); fragColor = color * (alpha * opacity);

View File

@ -1,2 +1,2 @@
// This file is generated. Edit build/generate-shaders.ts, then run `npm run codegen`. // This file is generated. Edit build/generate-shaders.ts, then run `npm run codegen`.
export default 'uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;uniform float u_sdfgamma;uniform float u_mix;in vec2 v_normal;in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);alpha*=smoothstep(0.5-u_sdfgamma/floorwidth,0.5+u_sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}'; export default 'uniform lowp float u_device_pixel_ratio;uniform lowp float u_lineatlas_width;uniform sampler2D u_image;uniform float u_mix;in vec2 v_normal;in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\n#pragma mapbox: define mediump vec4 dasharray_from\n#pragma mapbox: define mediump vec4 dasharray_to\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\n#pragma mapbox: initialize mediump vec4 dasharray_from\n#pragma mapbox: initialize mediump vec4 dasharray_to\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0/u_device_pixel_ratio)/min(dasharray_from.w,dasharray_to.w);alpha*=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}';

View File

@ -16,11 +16,11 @@ in vec4 a_data;
uniform vec2 u_translation; uniform vec2 u_translation;
uniform mediump float u_ratio; uniform mediump float u_ratio;
uniform lowp float u_device_pixel_ratio; uniform lowp float u_device_pixel_ratio;
uniform vec2 u_patternscale_a;
uniform float u_tex_y_a;
uniform vec2 u_patternscale_b;
uniform float u_tex_y_b;
uniform vec2 u_units_to_pixels; uniform vec2 u_units_to_pixels;
uniform float u_tileratio;
uniform float u_crossfade_from;
uniform float u_crossfade_to;
uniform float u_lineatlas_height;
out vec2 v_normal; out vec2 v_normal;
out vec2 v_width2; out vec2 v_width2;
@ -38,6 +38,8 @@ out float v_depth;
#pragma mapbox: define lowp float offset #pragma mapbox: define lowp float offset
#pragma mapbox: define mediump float width #pragma mapbox: define mediump float width
#pragma mapbox: define lowp float floorwidth #pragma mapbox: define lowp float floorwidth
#pragma mapbox: define mediump vec4 dasharray_from
#pragma mapbox: define mediump vec4 dasharray_to
void main() { void main() {
#pragma mapbox: initialize highp vec4 color #pragma mapbox: initialize highp vec4 color
@ -47,6 +49,8 @@ void main() {
#pragma mapbox: initialize lowp float offset #pragma mapbox: initialize lowp float offset
#pragma mapbox: initialize mediump float width #pragma mapbox: initialize mediump float width
#pragma mapbox: initialize lowp float floorwidth #pragma mapbox: initialize lowp float floorwidth
#pragma mapbox: initialize mediump vec4 dasharray_from
#pragma mapbox: initialize mediump vec4 dasharray_to
// the distance over which the line edge fades out. // the distance over which the line edge fades out.
// Retina devices need a smaller distance to avoid aliasing. // Retina devices need a smaller distance to avoid aliasing.
@ -103,7 +107,12 @@ void main() {
v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective;
#endif #endif
v_tex_a = vec2(a_linesofar * u_patternscale_a.x / floorwidth, normal.y * u_patternscale_a.y + u_tex_y_a); float u_patternscale_a_x = u_tileratio / dasharray_from.w / u_crossfade_from;
v_tex_b = vec2(a_linesofar * u_patternscale_b.x / floorwidth, normal.y * u_patternscale_b.y + u_tex_y_b); float u_patternscale_a_y = -dasharray_from.z / 2.0 / u_lineatlas_height;
float u_patternscale_b_x = u_tileratio / dasharray_to.w / u_crossfade_to;
float u_patternscale_b_y = -dasharray_to.z / 2.0 / u_lineatlas_height;
v_tex_a = vec2(a_linesofar * u_patternscale_a_x / floorwidth, normal.y * u_patternscale_a_y + (float(dasharray_from.y) + 0.5) / u_lineatlas_height);
v_tex_b = vec2(a_linesofar * u_patternscale_b_x / floorwidth, normal.y * u_patternscale_b_y + (float(dasharray_to.y) + 0.5) / u_lineatlas_height);
v_width2 = vec2(outset, inset); v_width2 = vec2(outset, inset);
} }

View File

@ -1,2 +1,2 @@
// This file is generated. Edit build/generate-shaders.ts, then run `npm run codegen`. // This file is generated. Edit build/generate-shaders.ts, then run `npm run codegen`.
export default '\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nin vec2 a_pos_normal;in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_patternscale_a;uniform float u_tex_y_a;uniform vec2 u_patternscale_b;uniform float u_tex_y_b;uniform vec2 u_units_to_pixels;out vec2 v_normal;out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_tex_a=vec2(a_linesofar*u_patternscale_a.x/floorwidth,normal.y*u_patternscale_a.y+u_tex_y_a);v_tex_b=vec2(a_linesofar*u_patternscale_b.x/floorwidth,normal.y*u_patternscale_b.y+u_tex_y_b);v_width2=vec2(outset,inset);}'; export default '\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nin vec2 a_pos_normal;in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\n#pragma mapbox: define mediump vec4 dasharray_from\n#pragma mapbox: define mediump vec4 dasharray_to\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\n#pragma mapbox: initialize mediump vec4 dasharray_from\n#pragma mapbox: initialize mediump vec4 dasharray_to\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nfloat u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}';

View File

@ -47,6 +47,8 @@ import linePatternFrag from './line_pattern.fragment.glsl.g';
import linePatternVert from './line_pattern.vertex.glsl.g'; import linePatternVert from './line_pattern.vertex.glsl.g';
import lineSDFFrag from './line_sdf.fragment.glsl.g'; import lineSDFFrag from './line_sdf.fragment.glsl.g';
import lineSDFVert from './line_sdf.vertex.glsl.g'; import lineSDFVert from './line_sdf.vertex.glsl.g';
import lineGradientSDFFrag from './line_gradient_sdf.fragment.glsl.g';
import lineGradientSDFVert from './line_gradient_sdf.vertex.glsl.g';
import rasterFrag from './raster.fragment.glsl.g'; import rasterFrag from './raster.fragment.glsl.g';
import rasterVert from './raster.vertex.glsl.g'; import rasterVert from './raster.vertex.glsl.g';
import symbolIconFrag from './symbol_icon.fragment.glsl.g'; import symbolIconFrag from './symbol_icon.fragment.glsl.g';
@ -104,6 +106,7 @@ export const shaders = {
lineGradient: prepare(lineGradientFrag, lineGradientVert), lineGradient: prepare(lineGradientFrag, lineGradientVert),
linePattern: prepare(linePatternFrag, linePatternVert), linePattern: prepare(linePatternFrag, linePatternVert),
lineSDF: prepare(lineSDFFrag, lineSDFVert), lineSDF: prepare(lineSDFFrag, lineSDFVert),
lineGradientSDF: prepare(lineGradientSDFFrag, lineGradientSDFVert),
raster: prepare(rasterFrag, rasterVert), raster: prepare(rasterFrag, rasterVert),
symbolIcon: prepare(symbolIconFrag, symbolIconVert), symbolIcon: prepare(symbolIconFrag, symbolIconVert),
symbolSDF: prepare(symbolSDFFrag, symbolSDFVert), symbolSDF: prepare(symbolSDFFrag, symbolSDFVert),

View File

@ -303,16 +303,12 @@ describe('GeoJSONSource.update', () => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
test('modifying cluster properties after adding a source', () => { test('modifying cluster properties after adding a source', async () => {
// test setCluster function on GeoJSONSource // test setCluster function on GeoJSONSource
const spy = vi.fn(); const spy = vi.fn();
const mockDispatcher = wrapDispatcher({ const mockDispatcher = wrapDispatcher({
sendAsync(message) { sendAsync(message) {
expect(message.type).toBe(MessageType.loadData); spy(message);
expect(message.data.cluster).toBe(true);
expect(message.data.superclusterOptions.radius).toBe(80 * EXTENT / source.tileSize);
expect(message.data.superclusterOptions.maxZoom).toBe(16);
spy();
return Promise.resolve({}); return Promise.resolve({});
} }
}); });
@ -325,8 +321,120 @@ describe('GeoJSONSource.update', () => {
clusterMinPoints: 3, clusterMinPoints: 3,
generateId: true generateId: true
}, mockDispatcher, undefined); }, mockDispatcher, undefined);
// Wait for initial data to be loaded
source.load();
await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
spy.mockClear();
source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16}); source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.cluster).toBe(true);
expect(spy.mock.calls[0][0].data.superclusterOptions.radius).toBe(80 * EXTENT / source.tileSize);
expect(spy.mock.calls[0][0].data.superclusterOptions.maxZoom).toBe(16);
});
test('modifying cluster properties with pending data', async () => {
const spy = vi.fn();
const mockDispatcher = wrapDispatcher({
sendAsync(message) {
spy(message);
return Promise.resolve({});
}
});
const source = new GeoJSONSource('id', {
type: 'geojson',
data: {} as GeoJSON.GeoJSON,
cluster: false,
clusterMaxZoom: 8,
clusterRadius: 100,
clusterMinPoints: 3,
generateId: true
}, mockDispatcher, undefined);
// Wait for initial data to be loaded
source.load();
await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
spy.mockClear();
// Initiate first data update
const sourceData1 = {id: 'test-1', type: 'FeatureCollection', features: []} as GeoJSON.GeoJSON;
source.setData(sourceData1);
// Immediately modify data again, and update cluster options
const sourceData2 = {id: 'test-2', type: 'FeatureCollection', features: []} as GeoJSON.GeoJSON;
source.setData(sourceData2);
source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});
await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.cluster).toBe(false);
expect(spy.mock.calls[0][0].data.data).toBe(JSON.stringify(sourceData1));
expect(spy.mock.calls[0][0].data.dataDiff).toBeUndefined();
expect(spy.mock.calls[1][0].data.cluster).toBe(true);
expect(spy.mock.calls[1][0].data.superclusterOptions.radius).toBe(80 * EXTENT / source.tileSize);
expect(spy.mock.calls[1][0].data.superclusterOptions.maxZoom).toBe(16);
expect(spy.mock.calls[1][0].data.data).toBe(JSON.stringify(sourceData2));
expect(spy.mock.calls[1][0].data.dataDiff).toBeUndefined();
});
test('modifying cluster properties after sending a diff', async () => {
const spy = vi.fn();
const mockDispatcher = wrapDispatcher({
sendAsync(message) {
spy(message);
return Promise.resolve({});
}
});
const geoJsonData = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'id': 1,
'properties': {},
'geometry': {
'type': 'Point',
'coordinates': [-122.48369693756104, 37.83381888486939]
}
}
]
} as GeoJSON.GeoJSON;
const source = new GeoJSONSource('id', {
type: 'geojson',
data: geoJsonData,
cluster: false,
clusterMaxZoom: 8,
clusterRadius: 100,
clusterMinPoints: 3,
generateId: true
}, mockDispatcher, undefined);
// Wait for initial data to be loaded
source.load();
await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
spy.mockReset();
const diff = {remove: [1]};
source.updateData(diff);
source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});
await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].data.cluster).toBe(false);
expect(spy.mock.calls[0][0].data.dataDiff).toEqual(diff);
expect(spy.mock.calls[1][0].data.cluster).toEqual(true);
expect(spy.mock.calls[1][0].data.data).not.toBeDefined();
expect(spy.mock.calls[1][0].data.dataDiff).not.toBeDefined();
}); });
test('forwards Supercluster options with worker request, ignore max zoom of source', () => { test('forwards Supercluster options with worker request, ignore max zoom of source', () => {
@ -831,6 +939,6 @@ describe('GeoJSONSource.load', () => {
source.load(); source.load();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith('No data or diff provided to GeoJSONSource id.'); expect(warnSpy).toHaveBeenCalledWith('No pending worker updates for GeoJSONSource id.');
}); });
}); });

View File

@ -126,7 +126,7 @@ export class GeoJSONSource extends Evented implements Source {
map: Map; map: Map;
actor: Actor; actor: Actor;
_isUpdatingWorker: boolean; _isUpdatingWorker: boolean;
_pendingWorkerUpdate: { data?: GeoJSON.GeoJSON | string; diff?: GeoJSONSourceDiff }; _pendingWorkerUpdate: { data?: GeoJSON.GeoJSON | string; diff?: GeoJSONSourceDiff; optionsChanged?: boolean };
_collectResourceTiming: boolean; _collectResourceTiming: boolean;
_removed: boolean; _removed: boolean;
@ -199,6 +199,10 @@ export class GeoJSONSource extends Evented implements Source {
} }
} }
private _hasPendingWorkerUpdate(): boolean {
return this._pendingWorkerUpdate.data !== undefined || this._pendingWorkerUpdate.diff !== undefined || this._pendingWorkerUpdate.optionsChanged;
}
private _pixelsToTileUnits(pixelValue: number): number { private _pixelsToTileUnits(pixelValue: number): number {
return pixelValue * (EXTENT / this.tileSize); return pixelValue * (EXTENT / this.tileSize);
} }
@ -309,12 +313,13 @@ export class GeoJSONSource extends Evented implements Source {
*/ */
setClusterOptions(options: SetClusterOptions): this { setClusterOptions(options: SetClusterOptions): this {
this.workerOptions.cluster = options.cluster; this.workerOptions.cluster = options.cluster;
if (options) { if (options.clusterRadius !== undefined) {
if (options.clusterRadius !== undefined) this.workerOptions.superclusterOptions.radius = this._pixelsToTileUnits(options.clusterRadius); this.workerOptions.superclusterOptions.radius = this._pixelsToTileUnits(options.clusterRadius);
if (options.clusterMaxZoom !== undefined) {
this.workerOptions.superclusterOptions.maxZoom = this._getClusterMaxZoom(options.clusterMaxZoom);
}
} }
if (options.clusterMaxZoom !== undefined) {
this.workerOptions.superclusterOptions.maxZoom = this._getClusterMaxZoom(options.clusterMaxZoom);
}
this._pendingWorkerUpdate.optionsChanged = true;
this._updateWorkerData(); this._updateWorkerData();
return this; return this;
} }
@ -382,13 +387,13 @@ export class GeoJSONSource extends Evented implements Source {
async _updateWorkerData(): Promise<void> { async _updateWorkerData(): Promise<void> {
if (this._isUpdatingWorker) return; if (this._isUpdatingWorker) return;
const {data, diff} = this._pendingWorkerUpdate; if (!this._hasPendingWorkerUpdate()) {
warnOnce(`No pending worker updates for GeoJSONSource ${this.id}.`);
if (!data && !diff) {
warnOnce(`No data or diff provided to GeoJSONSource ${this.id}.`);
return; return;
} }
const {data, diff} = this._pendingWorkerUpdate;
const options: LoadGeoJSONParameters = extend({type: this.type}, this.workerOptions); const options: LoadGeoJSONParameters = extend({type: this.type}, this.workerOptions);
if (data) { if (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
@ -404,6 +409,9 @@ export class GeoJSONSource extends Evented implements Source {
this._pendingWorkerUpdate.diff = undefined; this._pendingWorkerUpdate.diff = undefined;
} }
// Reset the flag since this update is using the latest options
this._pendingWorkerUpdate.optionsChanged = undefined;
this._isUpdatingWorker = true; this._isUpdatingWorker = true;
this.fire(new Event('dataloading', {dataType: 'source'})); this.fire(new Event('dataloading', {dataType: 'source'}));
try { try {
@ -439,14 +447,14 @@ export class GeoJSONSource extends Evented implements Source {
this.fire(new ErrorEvent(err)); this.fire(new ErrorEvent(err));
} finally { } finally {
// If there is more pending data, update worker again. // If there is more pending data, update worker again.
if (this._pendingWorkerUpdate.data || this._pendingWorkerUpdate.diff) { if (this._hasPendingWorkerUpdate()) {
this._updateWorkerData(); this._updateWorkerData();
} }
} }
} }
loaded(): boolean { loaded(): boolean {
return !this._isUpdatingWorker && this._pendingWorkerUpdate.data === undefined && this._pendingWorkerUpdate.diff === undefined; return !this._isUpdatingWorker && !this._hasPendingWorkerUpdate();
} }
async loadTile(tile: Tile): Promise<void> { async loadTile(tile: Tile): Promise<void> {

View File

@ -1,5 +1,5 @@
import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest'; import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
import {GeoJSONWorkerSource, type LoadGeoJSONParameters} from './geojson_worker_source'; import {createGeoJSONIndex, GeoJSONWorkerSource, type LoadGeoJSONParameters} from './geojson_worker_source';
import {StyleLayerIndex} from '../style/style_layer_index'; import {StyleLayerIndex} from '../style/style_layer_index';
import {OverscaledTileID} from './tile_id'; import {OverscaledTileID} from './tile_id';
import perf from '../util/performance'; import perf from '../util/performance';
@ -202,6 +202,30 @@ describe('loadData', () => {
properties: {}, properties: {},
} as GeoJSON.GeoJSON; } as GeoJSON.GeoJSON;
const updateableFeatureCollection = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'point1',
geometry: {
type: 'Point',
coordinates: [0, 0],
},
properties: {},
},
{
type: 'Feature',
id: 'point2',
geometry: {
type: 'Point',
coordinates: [1, 1],
},
properties: {},
}
]
} as GeoJSON.GeoJSON;
const layerIndex = new StyleLayerIndex(layers); const layerIndex = new StyleLayerIndex(layers);
function createWorker() { function createWorker() {
return new GeoJSONWorkerSource(actor, layerIndex, []); return new GeoJSONWorkerSource(actor, layerIndex, []);
@ -305,6 +329,29 @@ describe('loadData', () => {
}] }]
}} as LoadGeoJSONParameters)).resolves.toBeDefined(); }} as LoadGeoJSONParameters)).resolves.toBeDefined();
}); });
test('loadData should reject as first call with no data', async () => {
const worker = new GeoJSONWorkerSource(actor, layerIndex, []);
await expect(worker.loadData({} as LoadGeoJSONParameters)).rejects.toBeDefined();
});
test('loadData should resolve as subsequent call with no data', async () => {
const worker = new GeoJSONWorkerSource(actor, layerIndex, []);
await worker.loadData({source: 'source1', data: JSON.stringify(updateableGeoJson)} as LoadGeoJSONParameters);
await expect(worker.loadData({} as LoadGeoJSONParameters)).resolves.toBeDefined();
});
test('loadData should process cluster change with no data', async () => {
const mockCreateGeoJSONIndex = vi.fn(createGeoJSONIndex);
const worker = new GeoJSONWorkerSource(actor, layerIndex, [], mockCreateGeoJSONIndex);
await worker.loadData({source: 'source1', data: JSON.stringify(updateableFeatureCollection), cluster: false} as LoadGeoJSONParameters);
expect(mockCreateGeoJSONIndex.mock.calls[0][1].cluster).toBe(false);
await expect(worker.loadData({cluster: true} as LoadGeoJSONParameters)).resolves.toBeDefined();
expect(mockCreateGeoJSONIndex.mock.calls[1][1].cluster).toBe(true);
});
}); });
describe('getData', () => { describe('getData', () => {

View File

@ -18,6 +18,8 @@ import type {LoadVectorTileResult} from './vector_tile_worker_source';
import type {RequestParameters} from '../util/ajax'; import type {RequestParameters} from '../util/ajax';
import {isUpdateableGeoJSON, type GeoJSONSourceDiff, applySourceDiff, toUpdateable, type GeoJSONFeatureId} from './geojson_source_diff'; import {isUpdateableGeoJSON, type GeoJSONSourceDiff, applySourceDiff, toUpdateable, type GeoJSONFeatureId} from './geojson_source_diff';
import type {ClusterIDAndSource, GeoJSONWorkerSourceLoadDataResult, RemoveSourceParams} from '../util/actor_messages'; import type {ClusterIDAndSource, GeoJSONWorkerSourceLoadDataResult, RemoveSourceParams} from '../util/actor_messages';
import type {IActor} from '../util/actor';
import type {StyleLayerIndex} from '../style/style_layer_index';
/** /**
* The geojson worker options that can be passed to the worker * The geojson worker options that can be passed to the worker
@ -68,6 +70,12 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource {
_pendingRequest: AbortController; _pendingRequest: AbortController;
_geoJSONIndex: GeoJSONIndex; _geoJSONIndex: GeoJSONIndex;
_dataUpdateable = new Map<GeoJSONFeatureId, GeoJSON.Feature>(); _dataUpdateable = new Map<GeoJSONFeatureId, GeoJSON.Feature>();
_createGeoJSONIndex: typeof createGeoJSONIndex;
constructor(actor: IActor, layerIndex: StyleLayerIndex, availableImages: Array<string>, createGeoJSONIndexFunc: typeof createGeoJSONIndex = createGeoJSONIndex) {
super(actor, layerIndex, availableImages);
this._createGeoJSONIndex = createGeoJSONIndexFunc;
}
override async loadVectorTile(params: WorkerTileParameters, _abortController: AbortController): Promise<LoadVectorTileResult | null> { override async loadVectorTile(params: WorkerTileParameters, _abortController: AbortController): Promise<LoadVectorTileResult | null> {
const canonical = params.tileID.canonical; const canonical = params.tileID.canonical;
@ -82,8 +90,8 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource {
} }
const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features, {version: 2, extent: EXTENT}); const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features, {version: 2, extent: EXTENT});
// Encode the geojson-vt tile into binary vector tile form. This // Encode the geojson-vt tile into binary vector tile form.
// is a convenience that allows `FeatureIndex` to operate the same way // This is a convenience that allows `FeatureIndex` to operate the same way
// across `VectorTileSource` and `GeoJSONSource` data. // across `VectorTileSource` and `GeoJSONSource` data.
let pbf = fromVectorTileJs(geojsonWrapper); let pbf = fromVectorTileJs(geojsonWrapper);
if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) { if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) {
@ -100,7 +108,10 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource {
/** /**
* Fetches (if appropriate), parses, and index geojson data into tiles. This * Fetches (if appropriate), parses, and index geojson data into tiles. This
* preparatory method must be called before {@link GeoJSONWorkerSource.loadTile} * preparatory method must be called before {@link GeoJSONWorkerSource.loadTile}
* can correctly serve up tiles. * can correctly serve up tiles. The first call to this method must contain a valid
* {@link params.data}, {@link params.request}, or {@link params.dataDiff}. Subsequent
* calls may omit these parameters to reprocess the existing data (such as to update
* clustering options).
* *
* Defers to {@link GeoJSONWorkerSource.loadAndProcessGeoJSON} for the pre-processing. * Defers to {@link GeoJSONWorkerSource.loadAndProcessGeoJSON} for the pre-processing.
* *
@ -117,13 +128,15 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource {
this._pendingRequest = new AbortController(); this._pendingRequest = new AbortController();
try { try {
this._pendingData = this.loadAndProcessGeoJSON(params, this._pendingRequest); // Load and process data if no data has been loaded previously, or if there is
// a new request, data, or dataDiff to process.
if (!this._pendingData || params.request || params.data || params.dataDiff) {
this._pendingData = this.loadAndProcessGeoJSON(params, this._pendingRequest);
}
const data = await this._pendingData; const data = await this._pendingData;
this._geoJSONIndex = params.cluster ? this._geoJSONIndex = this._createGeoJSONIndex(data, params);
new Supercluster(getSuperclusterOptions(params)).load((data as any).features) :
geojsonvt(data, params.geojsonVtOptions);
this.loaded = {}; this.loaded = {};
@ -265,6 +278,11 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource {
} }
} }
export function createGeoJSONIndex(data: GeoJSON.GeoJSON, params: LoadGeoJSONParameters): GeoJSONIndex {
return params.cluster ? new Supercluster(getSuperclusterOptions(params)).load((data as any).features) :
geojsonvt(data, params.geojsonVtOptions);
}
function getSuperclusterOptions({superclusterOptions, clusterProperties}: LoadGeoJSONParameters) { function getSuperclusterOptions({superclusterOptions, clusterProperties}: LoadGeoJSONParameters) {
if (!clusterProperties || !superclusterOptions) return superclusterOptions; if (!clusterProperties || !superclusterOptions) return superclusterOptions;

View File

@ -1,17 +1,18 @@
import {describe, afterEach, test, expect, vi} from 'vitest'; import type {StyleSpecification} from '@maplibre/maplibre-gl-style-spec';
import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
import {SourceCache} from './source_cache'; import {SourceCache} from './source_cache';
import {type Map} from '../ui/map';
import {type Source, addSourceType} from './source'; import {type Source, addSourceType} from './source';
import {Tile} from './tile'; import {Tile, FadingRoles, FadingDirections} from './tile';
import {CanonicalTileID, OverscaledTileID} from './tile_id'; import {CanonicalTileID, OverscaledTileID} from './tile_id';
import {LngLat} from '../geo/lng_lat'; import {LngLat} from '../geo/lng_lat';
import Point from '@mapbox/point-geometry'; import Point from '@mapbox/point-geometry';
import {Event, ErrorEvent, Evented} from '../util/evented'; import {Event, ErrorEvent, Evented} from '../util/evented';
import {extend} from '../util/util'; import {extend} from '../util/util';
import {browser} from '../util/browser';
import {type Dispatcher} from '../util/dispatcher'; import {type Dispatcher} from '../util/dispatcher';
import {TileBounds} from './tile_bounds'; import {TileBounds} from './tile_bounds';
import {sleep, waitForEvent} from '../util/test/util'; import {sleep, waitForEvent, beforeMapTest, createMap as globalCreateMap} from '../util/test/util';
import {type Map} from '../ui/map';
import {type TileCache} from './tile_cache'; import {type TileCache} from './tile_cache';
import {MercatorTransform} from '../geo/projection/mercator_transform'; import {MercatorTransform} from '../geo/projection/mercator_transform';
import {GlobeTransform} from '../geo/projection/globe_transform'; import {GlobeTransform} from '../geo/projection/globe_transform';
@ -37,6 +38,9 @@ class SourceMock extends Evented implements Source {
if (sourceOptions.hasTile) { if (sourceOptions.hasTile) {
this.hasTile = sourceOptions.hasTile; this.hasTile = sourceOptions.hasTile;
} }
if (sourceOptions.raster) {
this.type = 'raster';
}
} }
loadTile(tile: Tile): Promise<void> { loadTile(tile: Tile): Promise<void> {
if (this.sourceOptions.expires) { if (this.sourceOptions.expires) {
@ -93,14 +97,27 @@ function createSourceCache(options?, used?) {
}, },
getTiles(): { [_: string]: Tile } { getTiles(): { [_: string]: Tile } {
return this._tiles; return this._tiles;
},
updateLoadedSiblingTileCache(): void {
this._updateLoadedSiblingTileCache();
} }
}); });
return scWithTestLogic; return scWithTestLogic;
} }
type MapOptions = {
style: StyleSpecification;
};
function createMap(options: MapOptions) {
const container = window.document.createElement('div');
window.document.body.appendChild(container);
Object.defineProperty(container, 'clientWidth', {value: 512});
Object.defineProperty(container, 'clientHeight', {value: 512});
return globalCreateMap({container, ...options});
}
beforeEach(() => {
beforeMapTest();
});
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@ -586,6 +603,55 @@ describe('SourceCache.update', () => {
expect(sourceCache.getIds()).toEqual([new OverscaledTileID(0, 0, 0, 0, 0).key]); expect(sourceCache.getIds()).toEqual([new OverscaledTileID(0, 0, 0, 0, 0).key]);
}); });
test('adds ideal (covering) tiles only once for zoom level on raster maps', async () => {
const transform = new MercatorTransform();
transform.resize(512, 512);
transform.setZoom(1);
const sourceCache = createSourceCache({raster: true});
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};
const addSpy = vi.spyOn(sourceCache, '_addTile');
const dataPromise = waitForEvent(sourceCache, 'data', e => e.sourceDataType === 'metadata');
sourceCache.onAdd(undefined);
await dataPromise;
// on update at zoom 1 there should be 4 ideal tiles added through _addTiles
sourceCache.update(transform);
expect(addSpy).toHaveBeenCalledTimes(4);
});
test('bypasses fading logic when raster fading is disabled', async () => {
const map = createMap({
style: {
version: 8,
sources: {rasterSource: {type: 'raster', tiles: [], tileSize: 256}},
layers: [{id: 'rasterLayer', type: 'raster', source: 'rasterSource',
paint: {'raster-fade-duration': 0}
}]
}
});
await map.once('styledata');
const style = map.style;
const sourceCache = style.sourceCaches['rasterSource'];
const spy = vi.spyOn(sourceCache, '_updateFadingTiles');
sourceCache._loadTile = async () => {};
const fakeTile = new Tile(new OverscaledTileID(3, 0, 3, 1, 2), undefined);
(fakeTile as any).texture = {bind: () => {}, size: [256, 256]};
fakeTile.state = 'loaded';
sourceCache._tiles[fakeTile.tileID.key] = fakeTile;
await map.once('render');
map.setZoom(3);
await map.once('render');
expect(spy).not.toHaveBeenCalled();
});
test('respects Source.hasTile method if it is present', async () => { test('respects Source.hasTile method if it is present', async () => {
const transform = new MercatorTransform(); const transform = new MercatorTransform();
transform.resize(511, 511); transform.resize(511, 511);
@ -691,131 +757,6 @@ describe('SourceCache.update', () => {
]); ]);
}); });
test('retains covered child tiles while parent tile is fading in', async () => {
const transform = new MercatorTransform();
transform.resize(511, 511);
transform.setZoom(2);
const sourceCache = createSourceCache();
sourceCache._source.loadTile = async (tile) => {
tile.timeAdded = Infinity;
tile.state = 'loaded';
tile.registerFadeDuration(100);
};
(sourceCache._source as any).type = 'raster';
const dataPromise = waitForEvent(sourceCache, 'data', e => e.sourceDataType === 'metadata');
sourceCache.onAdd(undefined);
await dataPromise;
sourceCache.update(transform);
expect(sourceCache.getIds()).toEqual([
new OverscaledTileID(2, 0, 2, 2, 2).key,
new OverscaledTileID(2, 0, 2, 1, 2).key,
new OverscaledTileID(2, 0, 2, 2, 1).key,
new OverscaledTileID(2, 0, 2, 1, 1).key
]);
transform.setZoom(0);
sourceCache.update(transform);
expect(sourceCache.getRenderableIds()).toHaveLength(5);
});
test('retains a parent tile for fading even if a tile is partially covered by children', async () => {
const transform = new MercatorTransform();
transform.resize(511, 511);
transform.setZoom(0);
const sourceCache = createSourceCache();
sourceCache._source.loadTile = async (tile) => {
tile.timeAdded = Infinity;
tile.state = 'loaded';
tile.registerFadeDuration(100);
};
(sourceCache._source as any).type = 'raster';
const dataPromise = waitForEvent(sourceCache, 'data', e => e.sourceDataType === 'metadata');
sourceCache.onAdd(undefined);
await dataPromise;
sourceCache.update(transform);
transform.setZoom(2);
sourceCache.update(transform);
transform.setZoom(1);
sourceCache.update(transform);
expect(sourceCache._coveredTiles[(new OverscaledTileID(0, 0, 0, 0, 0).key)]).toBe(true);
});
test('retain children for fading fadeEndTime is 0 (added but registerFadeDuration() is not called yet)', async () => {
const transform = new MercatorTransform();
transform.resize(511, 511);
transform.setZoom(1);
const sourceCache = createSourceCache();
sourceCache._source.loadTile = async (tile) => {
// not setting fadeEndTime because class Tile default is 0, and need to be tested
tile.timeAdded = Date.now();
tile.state = 'loaded';
};
(sourceCache._source as any).type = 'raster';
const dataPromise = waitForEvent(sourceCache, 'data', e => e.sourceDataType === 'metadata');
sourceCache.onAdd(undefined);
await dataPromise;
sourceCache.update(transform);
transform.setZoom(0);
sourceCache.update(transform);
expect(sourceCache.getRenderableIds()).toHaveLength(5);
});
test('retains children when tile.fadeEndTime is in the future', async () => {
const transform = new MercatorTransform();
transform.resize(511, 511);
transform.setZoom(1);
const fadeTime = 100;
const start = Date.now();
let time = start;
vi.spyOn(browser, 'now').mockImplementation(() => time);
const sourceCache = createSourceCache();
sourceCache._source.loadTile = async (tile) => {
tile.timeAdded = browser.now();
tile.state = 'loaded';
tile.fadeEndTime = browser.now() + fadeTime;
};
(sourceCache._source as any).type = 'raster';
const dataPromise = waitForEvent(sourceCache, 'data', e => e.sourceDataType === 'metadata');
sourceCache.onAdd(undefined);
await dataPromise;
// load children
sourceCache.update(transform);
transform.setZoom(0);
sourceCache.update(transform);
expect(sourceCache.getRenderableIds()).toHaveLength(5);
time = start + 98;
sourceCache.update(transform);
expect(sourceCache.getRenderableIds()).toHaveLength(5);
time = start + fadeTime + 1;
sourceCache.update(transform);
expect(sourceCache.getRenderableIds()).toHaveLength(1);
});
test('retains children tiles for pending parents', () => { test('retains children tiles for pending parents', () => {
const transform = new GlobeTransform(); const transform = new GlobeTransform();
transform.resize(511, 511); transform.resize(511, 511);
@ -910,6 +851,173 @@ describe('SourceCache.update', () => {
expect(sourceCache.getTile(wrappedTileID)).toBe(tile); expect(sourceCache.getTile(wrappedTileID)).toBe(tile);
}); });
test('retains fading children and applies fading logic when zooming out', async () => {
const transform = new MercatorTransform();
transform.resize(1024, 1024);
transform.setZoom(10);
const sourceCache = createSourceCache({raster: true});
const loadedTiles: Record<string, Tile> = {};
sourceCache._source.loadTile = async (tile) => {
loadedTiles[tile.tileID.key] = tile;
tile.state = 'loaded';
};
sourceCache.on('data', (e) => {
if (e.dataType === 'source' && e.sourceDataType === 'metadata') {
sourceCache.update(transform);
}
});
sourceCache.setRasterFadeDuration(300);
sourceCache.onAdd(undefined);
// get default zoom ideal tiles at zoom specified above
await sleep(0);
// ideal tiles will become fading children when zooming out
const children: Tile[] = Object.values(loadedTiles);
// zoom out 1 level - ideal tiles (new children) should fade out
transform.setZoom(9);
sourceCache.update(transform);
await sleep(0);
// ensure that the loaded child was retained and fading logic was applied
for (const child of children) {
expect(loadedTiles).toHaveProperty(child.tileID.key);
expect(child.fadingRole).toEqual(FadingRoles.Base);
expect(child.fadingDirection).toEqual(FadingDirections.Departing);
expect(child.fadingParentID).toBeInstanceOf(OverscaledTileID);
}
});
test('retains fading grandchildren and applies fading logic when zooming out', async () => {
const transform = new MercatorTransform();
transform.resize(512, 512);
transform.setZoom(10);
const sourceCache = createSourceCache({raster: true});
const loadedTiles: Record<string, Tile> = {};
sourceCache._source.loadTile = async (tile) => {
loadedTiles[tile.tileID.key] = tile;
tile.state = 'loaded';
};
sourceCache.on('data', (e) => {
if (e.dataType === 'source' && e.sourceDataType === 'metadata') {
sourceCache.update(transform);
}
});
sourceCache.setRasterFadeDuration(300);
sourceCache.onAdd(undefined);
// get default zoom ideal tiles at zoom specified above
await sleep(0);
// ideal tiles will become fading grandchildren when zooming out
const grandChildren: Tile[] = Object.values(loadedTiles);
// zoom out 2 levels - ideal tiles (new grandchildren) should fade out
transform.setZoom(8);
sourceCache.update(transform);
await sleep(0);
// ensure that the loaded grandchild was retained and fading logic was applied
for (const grandChild of grandChildren) {
expect(loadedTiles).toHaveProperty(grandChild.tileID.key);
expect(grandChild.fadingRole).toEqual(FadingRoles.Base);
expect(grandChild.fadingDirection).toEqual(FadingDirections.Departing);
expect(grandChild.fadingParentID).toBeInstanceOf(OverscaledTileID);
}
});
test('retains fading parent and applies fading logic when zooming in', async () => {
const transform = new MercatorTransform();
transform.resize(512, 512);
transform.setZoom(10);
const sourceCache = createSourceCache({raster: true});
const loadedTiles: Record<string, Tile> = {};
sourceCache._source.loadTile = async (tile) => {
loadedTiles[tile.tileID.key] = tile;
tile.state = 'loaded';
};
sourceCache.on('data', (e) => {
if (e.dataType === 'source' && e.sourceDataType === 'metadata') {
sourceCache.update(transform);
}
});
sourceCache.setRasterFadeDuration(300);
sourceCache.onAdd(undefined);
// get default zoom ideal tiles at zoom specified above
await sleep(0);
// ideal tiles will become fading parent when zooming in
const parents: Tile[] = Object.values(loadedTiles);
const parentKeys = new Set(parents.map(p => p.tileID.key));
// zoom in 1 level - ideal tiles (new parent) should fade out
transform.setZoom(11);
sourceCache.update(transform);
await sleep(0);
// ensure that the loaded parents were retained and fading logic was applied
for (const parent of parents) {
expect(loadedTiles).toHaveProperty(parent.tileID.key);
expect(parent.fadingRole).toEqual(FadingRoles.Parent);
expect(parent.fadingDirection).toEqual(FadingDirections.Departing);
}
// check incoming tiles
const incoming = Object.values(loadedTiles).filter(tile => !parentKeys.has(tile.tileID.key));
for (const tile of incoming) {
expect(tile.fadingRole).toEqual(FadingRoles.Base);
expect(tile.fadingDirection).toEqual(FadingDirections.Incoming);
expect(tile.fadingParentID).toBeInstanceOf(OverscaledTileID);
}
});
test('retains fading grandparent and applies fading logic when zooming in', async () => {
const transform = new MercatorTransform();
transform.resize(512, 512);
transform.setZoom(10);
const sourceCache = createSourceCache({raster: true});
const loadedTiles: Record<string, Tile> = {};
sourceCache._source.loadTile = async (tile) => {
loadedTiles[tile.tileID.key] = tile;
tile.state = 'loaded';
};
sourceCache.on('data', (e) => {
if (e.dataType === 'source' && e.sourceDataType === 'metadata') {
sourceCache.update(transform);
}
});
sourceCache.setRasterFadeDuration(300);
sourceCache.onAdd(undefined);
// get default zoom ideal tiles at zoom specified above
await sleep(0);
// ideal tiles will become fading grandparent when zooming in
const grandParents: Tile[] = Object.values(loadedTiles);
const grandParentKeys = new Set(grandParents.map(p => p.tileID.key));
// zoom in 2 levels - ideal tiles (new grandparent) should fade out
transform.setZoom(12);
sourceCache.update(transform);
await sleep(0);
// ensure that the loaded grandparents were retained and fading logic was applied
for (const grandParent of grandParents) {
expect(loadedTiles).toHaveProperty(grandParent.tileID.key);
expect(grandParent.fadingRole).toEqual(FadingRoles.Parent);
expect(grandParent.fadingDirection).toEqual(FadingDirections.Departing);
}
// check incoming tiles
const incoming = Object.values(loadedTiles).filter(tile => !grandParentKeys.has(tile.tileID.key));
for (const tile of incoming) {
expect(tile.fadingRole).toEqual(FadingRoles.Base);
expect(tile.fadingDirection).toEqual(FadingDirections.Incoming);
expect(tile.fadingParentID).toBeInstanceOf(OverscaledTileID);
}
});
}); });
describe('SourceCache._updateRetainedTiles', () => { describe('SourceCache._updateRetainedTiles', () => {
@ -971,6 +1079,31 @@ describe('SourceCache._updateRetainedTiles', () => {
expect(Object.keys(retained).sort()).toEqual(expectedTiles.map(t => t.key).sort()); expect(Object.keys(retained).sort()).toEqual(expectedTiles.map(t => t.key).sort());
}); });
test('_updateRetainedTiles does not retain parents when 2nd generation children are loaded', () => {
const sourceCache = createSourceCache();
sourceCache._source.loadTile = async (tile) => {
tile.state = 'errored';
};
const idealTile = new OverscaledTileID(3, 0, 3, 1, 2);
sourceCache._tiles[idealTile.key] = new Tile(idealTile, undefined);
sourceCache._tiles[idealTile.key].state = 'errored';
const secondGeneration = idealTile
.children(10)
.flatMap(child => child.children(10));
expect(secondGeneration.length).toEqual(16);
for (const id of secondGeneration) {
sourceCache._tiles[id.key] = new Tile(id, undefined);
sourceCache._tiles[id.key].state = 'loaded';
}
const expectedTiles = [...secondGeneration, idealTile];
const retained = sourceCache._updateRetainedTiles([idealTile], 3);
expect(Object.keys(retained).sort()).toEqual(expectedTiles.map(t => t.key).sort());
});
for (const pitch of [0, 20, 40, 65, 75, 85]) { for (const pitch of [0, 20, 40, 65, 75, 85]) {
test(`retains loaded children for pitch: ${pitch}`, () => { test(`retains loaded children for pitch: ${pitch}`, () => {
const transform = new MercatorTransform(); const transform = new MercatorTransform();
@ -1070,6 +1203,38 @@ describe('SourceCache._updateRetainedTiles', () => {
expect(Object.keys(retained).sort()).toEqual([idealTile].concat(loadedChildren).map(t => t.key).sort()); expect(Object.keys(retained).sort()).toEqual([idealTile].concat(loadedChildren).map(t => t.key).sort());
}); });
test('_areDescendentsComplete returns true when descendents fully cover a generation', () => {
const sourceCache = createSourceCache();
const idealTile = new OverscaledTileID(3, 0, 3, 1, 2);
const firstGen = idealTile.children(10);
expect(sourceCache._areDescendentsComplete(firstGen, 4, 3)).toBe(true);
const secondGen = idealTile.children(10).flatMap(c => c.children(10));
expect(sourceCache._areDescendentsComplete(secondGen, 5, 3)).toBe(true);
});
test('_areDescendentsComplete returns false when descendents are incomplete', () => {
const sourceCache = createSourceCache();
const idealTile = new OverscaledTileID(3, 0, 3, 1, 2);
const firstGenPartial = idealTile.children(10).slice(0, 3);
expect(sourceCache._areDescendentsComplete(firstGenPartial, 4, 3)).toBe(false);
const secondGenPartial = idealTile.children(10).flatMap(c => c.children(10)).slice(0, 15);
expect(sourceCache._areDescendentsComplete(secondGenPartial, 5, 3)).toBe(false);
});
test('_areDescendentsComplete properly handles overscaled tiles', () => {
const sourceCache = createSourceCache();
const correct = new OverscaledTileID(4, 0, 3, 1, 2);
expect(sourceCache._areDescendentsComplete([correct], 4, 3)).toBe(true);
const wrong = new OverscaledTileID(5, 0, 3, 1, 2);
expect(sourceCache._areDescendentsComplete([wrong], 4, 3)).toBe(false);
});
test('adds parent tile if ideal tile errors and no child tiles are loaded', () => { test('adds parent tile if ideal tile errors and no child tiles are loaded', () => {
const stateCache = {}; const stateCache = {};
const sourceCache = createSourceCache(); const sourceCache = createSourceCache();
@ -1272,22 +1437,22 @@ describe('SourceCache._updateRetainedTiles', () => {
}; };
const idealTile = new OverscaledTileID(2, 0, 2, 0, 0); const idealTile = new OverscaledTileID(2, 0, 2, 0, 0);
const getTileSpy = vi.spyOn(sourceCache, 'getTile'); const loadedTiles = [
const retained = sourceCache._updateRetainedTiles([idealTile], 2); new OverscaledTileID(3, 0, 2, 0, 0), // overzoomed child
new OverscaledTileID(1, 0, 1, 0, 0), // parent
expect(getTileSpy.mock.calls.map((c) => { return c[0]; })).toEqual([ new OverscaledTileID(0, 0, 0, 0, 0) // parent
// overzoomed child ];
new OverscaledTileID(3, 0, 2, 0, 0), loadedTiles.forEach(t => {
// parents sourceCache._tiles[t.key] = new Tile(t, undefined);
new OverscaledTileID(1, 0, 1, 0, 0), sourceCache._tiles[t.key].state = 'loaded';
new OverscaledTileID(0, 0, 0, 0, 0)
]);
expect(retained).toEqual({
// ideal tile id (2, 0, 0)
'022': new OverscaledTileID(2, 0, 2, 0, 0)
}); });
const retained = sourceCache._updateRetainedTiles([idealTile], 2);
expect(retained).toEqual({
'022': new OverscaledTileID(2, 0, 2, 0, 0), // ideal
'023': new OverscaledTileID(3, 0, 2, 0, 0) // overzoomed
});
}); });
test('don\'t ascend multiple times if a tile is not found', () => { test('don\'t ascend multiple times if a tile is not found', () => {
@ -2138,191 +2303,6 @@ describe('source cache get ids', () => {
}); });
}); });
describe('SourceCache.findLoadedParent', () => {
test('adds from previously used tiles (sourceCache._tiles)', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const tile = {
tileID: new OverscaledTileID(1, 0, 1, 0, 0),
hasData() { return true; }
} as any as Tile;
sourceCache._tiles[tile.tileID.key] = tile;
expect(sourceCache.findLoadedParent(new OverscaledTileID(2, 0, 2, 3, 3), 0)).toBeUndefined();
expect(sourceCache.findLoadedParent(new OverscaledTileID(2, 0, 2, 0, 0), 0)).toEqual(tile);
});
test('retains parents', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const tile = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512);
sourceCache._cache.add(tile.tileID, tile);
expect(sourceCache.findLoadedParent(new OverscaledTileID(2, 0, 2, 3, 3), 0)).toBeUndefined();
expect(sourceCache.findLoadedParent(new OverscaledTileID(2, 0, 2, 0, 0), 0)).toBe(tile);
expect(sourceCache._cache.order).toHaveLength(1);
});
test('Search cache for loaded parent tiles', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const mockTile = id => {
const tile = {
tileID: id,
hasData() { return true; }
} as any as Tile;
sourceCache._tiles[id.key] = tile;
};
const tiles = [
new OverscaledTileID(0, 0, 0, 0, 0),
new OverscaledTileID(1, 0, 1, 1, 0),
new OverscaledTileID(2, 0, 2, 0, 0),
new OverscaledTileID(2, 0, 2, 1, 0),
new OverscaledTileID(2, 0, 2, 2, 0),
new OverscaledTileID(2, 0, 2, 1, 2)
];
tiles.forEach(t => mockTile(t));
sourceCache._updateLoadedParentTileCache();
// Loaded tiles excluding the root should be in the cache
expect(sourceCache.findLoadedParent(tiles[0], 0)).toBeUndefined();
expect(sourceCache.findLoadedParent(tiles[1], 0).tileID).toBe(tiles[0]);
expect(sourceCache.findLoadedParent(tiles[2], 0).tileID).toBe(tiles[0]);
expect(sourceCache.findLoadedParent(tiles[3], 0).tileID).toBe(tiles[0]);
expect(sourceCache.findLoadedParent(tiles[4], 0).tileID).toBe(tiles[1]);
expect(sourceCache.findLoadedParent(tiles[5], 0).tileID).toBe(tiles[0]);
expect(tiles[0].key in sourceCache._loadedParentTiles).toBe(false);
expect(tiles[1].key in sourceCache._loadedParentTiles).toBe(true);
expect(tiles[2].key in sourceCache._loadedParentTiles).toBe(true);
expect(tiles[3].key in sourceCache._loadedParentTiles).toBe(true);
expect(tiles[4].key in sourceCache._loadedParentTiles).toBe(true);
expect(tiles[5].key in sourceCache._loadedParentTiles).toBe(true);
// Arbitrary tiles should not in the cache
const notLoadedTiles = [
new OverscaledTileID(2, 1, 2, 0, 0),
new OverscaledTileID(2, 0, 2, 3, 0),
new OverscaledTileID(2, 0, 2, 3, 3),
new OverscaledTileID(3, 0, 3, 2, 1)
];
expect(sourceCache.findLoadedParent(notLoadedTiles[0], 0)).toBeUndefined();
expect(sourceCache.findLoadedParent(notLoadedTiles[1], 0).tileID).toBe(tiles[1]);
expect(sourceCache.findLoadedParent(notLoadedTiles[2], 0).tileID).toBe(tiles[0]);
expect(sourceCache.findLoadedParent(notLoadedTiles[3], 0).tileID).toBe(tiles[3]);
expect(notLoadedTiles[0].key in sourceCache._loadedParentTiles).toBe(false);
expect(notLoadedTiles[1].key in sourceCache._loadedParentTiles).toBe(false);
expect(notLoadedTiles[2].key in sourceCache._loadedParentTiles).toBe(false);
expect(notLoadedTiles[3].key in sourceCache._loadedParentTiles).toBe(false);
});
});
describe('SourceCache.findLoadedSibling', () => {
test('adds from previously used tiles (sourceCache._tiles)', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const tile = {
tileID: new OverscaledTileID(1, 0, 1, 0, 0),
hasData() { return true; }
} as any as Tile;
sourceCache.getTiles()[tile.tileID.key] = tile;
expect(sourceCache.findLoadedSibling(new OverscaledTileID(1, 0, 1, 1, 0))).toBeNull();
expect(sourceCache.findLoadedSibling(new OverscaledTileID(1, 0, 1, 0, 0))).toEqual(tile);
});
test('retains siblings', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const tile = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512);
sourceCache.getCache().add(tile.tileID, tile);
expect(sourceCache.findLoadedSibling(new OverscaledTileID(1, 0, 1, 1, 0))).toBeNull();
expect(sourceCache.findLoadedSibling(new OverscaledTileID(1, 0, 1, 0, 0))).toBe(tile);
expect(sourceCache.getCache().order).toHaveLength(1);
});
test('Search cache for loaded sibling tiles', () => {
const sourceCache = createSourceCache({});
sourceCache.onAdd(undefined);
const tr = new MercatorTransform();
tr.resize(512, 512);
sourceCache.updateCacheSize(tr);
const mockTile = id => {
const tile = {
tileID: id,
hasData() { return true; }
} as any as Tile;
sourceCache.getTiles()[id.key] = tile;
};
const tiles = [
new OverscaledTileID(0, 0, 0, 0, 0),
new OverscaledTileID(1, 0, 1, 1, 0),
new OverscaledTileID(2, 0, 2, 0, 0),
new OverscaledTileID(2, 0, 2, 1, 0),
new OverscaledTileID(2, 0, 2, 2, 0),
new OverscaledTileID(2, 0, 2, 1, 2)
];
tiles.forEach(t => mockTile(t));
sourceCache.updateLoadedSiblingTileCache();
// Loaded tiles should be in the cache
expect(sourceCache.findLoadedSibling(tiles[0]).tileID).toBe(tiles[0]);
expect(sourceCache.findLoadedSibling(tiles[1]).tileID).toBe(tiles[1]);
expect(sourceCache.findLoadedSibling(tiles[2]).tileID).toBe(tiles[2]);
expect(sourceCache.findLoadedSibling(tiles[3]).tileID).toBe(tiles[3]);
expect(sourceCache.findLoadedSibling(tiles[4]).tileID).toBe(tiles[4]);
expect(sourceCache.findLoadedSibling(tiles[5]).tileID).toBe(tiles[5]);
// Arbitrary tiles should not in the cache
const notLoadedTiles = [
new OverscaledTileID(2, 1, 2, 0, 0),
new OverscaledTileID(2, 0, 2, 3, 0),
new OverscaledTileID(2, 0, 2, 3, 3),
new OverscaledTileID(3, 0, 3, 2, 1)
];
expect(sourceCache.findLoadedSibling(notLoadedTiles[0])).toBeNull();
expect(sourceCache.findLoadedSibling(notLoadedTiles[1])).toBeNull();
expect(sourceCache.findLoadedSibling(notLoadedTiles[2])).toBeNull();
expect(sourceCache.findLoadedSibling(notLoadedTiles[3])).toBeNull();
});
});
describe('SourceCache.reload', () => { describe('SourceCache.reload', () => {
test('before loaded', () => { test('before loaded', () => {
const sourceCache = createSourceCache({noLoad: true}); const sourceCache = createSourceCache({noLoad: true});
@ -2562,5 +2542,4 @@ describe('SourceCache::refreshTiles', () => {
expect(spy.mock.calls[2][1]).toBe('expired'); expect(spy.mock.calls[2][1]).toBe('expired');
expect(spy.mock.calls[3][1]).toBe('expired'); expect(spy.mock.calls[3][1]).toBe('expired');
}); });
}); });

View File

@ -1,16 +1,16 @@
import {create as createSource} from './source'; import {create as createSource} from './source';
import {Tile} from './tile'; import {Tile, FadingDirections, FadingRoles} from './tile';
import {Event, ErrorEvent, Evented} from '../util/evented'; import {ErrorEvent, Event, Evented} from '../util/evented';
import {TileCache} from './tile_cache'; import {TileCache} from './tile_cache';
import {MercatorCoordinate} from '../geo/mercator_coordinate'; import {MercatorCoordinate} from '../geo/mercator_coordinate';
import {keysDifference} from '../util/util';
import {EXTENT} from '../data/extent'; import {EXTENT} from '../data/extent';
import {type Context} from '../gl/context'; import {type Context} from '../gl/context';
import Point from '@mapbox/point-geometry'; import Point from '@mapbox/point-geometry';
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {OverscaledTileID} from './tile_id'; import {OverscaledTileID} from './tile_id';
import {SourceFeatureState} from './source_state'; import {SourceFeatureState} from './source_state';
import {getEdgeTiles} from '../util/util';
import {config} from '../util/config'; import {config} from '../util/config';
import type {Source} from './source'; import type {Source} from './source';
@ -62,30 +62,24 @@ export class SourceCache extends Evented {
_sourceLoaded: boolean; _sourceLoaded: boolean;
_sourceErrored: boolean; _sourceErrored: boolean;
_tiles: {[_: string]: Tile}; _tiles: Record<string, Tile>;
_prevLng: number; _prevLng: number;
_cache: TileCache; _cache: TileCache;
_timers: { _timers: Record<string, ReturnType<typeof setTimeout>>;
[_ in any]: ReturnType<typeof setTimeout>;
};
_cacheTimers: {
[_ in any]: ReturnType<typeof setTimeout>;
};
_maxTileCacheSize: number; _maxTileCacheSize: number;
_maxTileCacheZoomLevels: number; _maxTileCacheZoomLevels: number;
_paused: boolean; _paused: boolean;
_shouldReloadOnResume: boolean; _shouldReloadOnResume: boolean;
_coveredTiles: {[_: string]: boolean};
transform: ITransform; transform: ITransform;
terrain: Terrain; terrain: Terrain;
used: boolean; used: boolean;
usedForTerrain: boolean; usedForTerrain: boolean;
tileSize: number; tileSize: number;
_state: SourceFeatureState; _state: SourceFeatureState;
_loadedParentTiles: {[_: string]: Tile};
_loadedSiblingTiles: {[_: string]: Tile};
_didEmitContent: boolean; _didEmitContent: boolean;
_updated: boolean; _updated: boolean;
_rasterFadeDuration: number;
_maxFadingAncestorLevels: number;
static maxUnderzooming: number; static maxUnderzooming: number;
static maxOverzooming: number; static maxOverzooming: number;
@ -111,12 +105,11 @@ export class SourceCache extends Evented {
this._tiles = {}; this._tiles = {};
this._cache = new TileCache(0, (tile) => this._unloadTile(tile)); this._cache = new TileCache(0, (tile) => this._unloadTile(tile));
this._timers = {}; this._timers = {};
this._cacheTimers = {};
this._maxTileCacheSize = null; this._maxTileCacheSize = null;
this._maxTileCacheZoomLevels = null; this._maxTileCacheZoomLevels = null;
this._loadedParentTiles = {}; this._rasterFadeDuration = 0;
this._maxFadingAncestorLevels = 5;
this._coveredTiles = {};
this._state = new SourceFeatureState(); this._state = new SourceFeatureState();
this._didEmitContent = false; this._didEmitContent = false;
this._updated = false; this._updated = false;
@ -223,7 +216,7 @@ export class SourceCache extends Evented {
* Return all tile ids ordered with z-order, and cast to numbers * Return all tile ids ordered with z-order, and cast to numbers
*/ */
getIds(): Array<string> { getIds(): Array<string> {
return (Object.values(this._tiles) as any).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); return Object.values(this._tiles).map(tile => tile.tileID).sort(compareTileId).map(id => id.key);
} }
getRenderableIds(symbolLayer?: boolean): Array<string> { getRenderableIds(symbolLayer?: boolean): Array<string> {
@ -244,18 +237,24 @@ export class SourceCache extends Evented {
} }
hasRenderableParent(tileID: OverscaledTileID) { hasRenderableParent(tileID: OverscaledTileID) {
const parentTile = this.findLoadedParent(tileID, 0); const parentZ = tileID.overscaledZ - 1;
if (parentTile) { if (parentZ >= this._source.minzoom) {
return this._isIdRenderable(parentTile.tileID.key); const parentTile = this._getLoadedTile(tileID.scaledTo(parentZ));
if (parentTile) {
return this._isIdRenderable(parentTile.tileID.key);
}
} }
return false; return false;
} }
_isIdRenderable(id: string, symbolLayer?: boolean) { _isIdRenderable(id: string, symbolLayer: boolean = false) {
return this._tiles[id] && this._tiles[id].hasData() && return this._tiles[id]?.isRenderable(symbolLayer);
!this._coveredTiles[id] && (symbolLayer || !this._tiles[id].holdingForFade());
} }
/**
* Reload tiles in this source. If source data has changed, reload all tiles using a state of 'expired',
* otherwise reload only non-errored tiles using state of 'reloading'.
*/
reload(sourceDataChanged?: boolean) { reload(sourceDataChanged?: boolean) {
if (this._paused) { if (this._paused) {
this._shouldReloadOnResume = true; this._shouldReloadOnResume = true;
@ -292,7 +291,7 @@ export class SourceCache extends Evented {
} }
_tileLoaded(tile: Tile, id: string, previousState: TileState) { _tileLoaded(tile: Tile, id: string, previousState: TileState) {
tile.timeAdded = browser.now(); tile.timeAdded = now();
if (previousState === 'expired') tile.refreshedUponExpiration = true; if (previousState === 'expired') tile.refreshedUponExpiration = true;
this._setTileReloadTimer(id, tile); this._setTileReloadTimer(id, tile);
if (this.getSource().type === 'raster-dem' && tile.dem) this._backfillDEM(tile); if (this.getSource().type === 'raster-dem' && tile.dem) this._backfillDEM(tile);
@ -379,44 +378,50 @@ export class SourceCache extends Evented {
*/ */
_retainLoadedChildren( _retainLoadedChildren(
targetTiles: { [_: string]: OverscaledTileID }, targetTiles: Record<string, OverscaledTileID>,
retain: { [_: string]: OverscaledTileID } retain: Record<string, OverscaledTileID>
) { ) {
const targetTileIDs = Object.values(targetTiles); const targetTileIDs = Object.values(targetTiles);
const loadedDescendents: { [_: string]: Tile[] } = this._getLoadedDescendents(targetTileIDs); const loadedDescendents: Record<string, Tile[]> = this._getLoadedDescendents(targetTileIDs);
const incomplete: Record<string, OverscaledTileID> = {};
// retain the uppermost descendents of target tiles // retain the uppermost descendents of target tiles
for (const targetID of targetTileIDs) { for (const targetID of targetTileIDs) {
const descendentTiles = loadedDescendents[targetID.key]; const descendents = loadedDescendents[targetID.key];
if (!descendentTiles) continue; if (!descendents?.length) {
incomplete[targetID.key] = targetID;
const targetTileMaxCoveringZoom = targetID.overscaledZ + SourceCache.maxUnderzooming; continue;
// determine the topmost zoom (overscaledZ) in the set of descendent tiles. (i.e. zoom 4 tiles are topmost relative to zoom 5)
let topmostZoom = Infinity;
for (const tile of descendentTiles) {
const zoom = tile.tileID.overscaledZ;
if (zoom <= targetTileMaxCoveringZoom && zoom < topmostZoom) {
topmostZoom = zoom;
}
} }
// retain all uppermost descendents (with the same overscaledZ) in the topmost zoom below the target tile // find descendents within the max covering zoom range
if (topmostZoom !== Infinity) { const maxCoveringZoom = targetID.overscaledZ + SourceCache.maxUnderzooming;
for (const tile of descendentTiles) { const candidates = descendents.filter(t => t.tileID.overscaledZ <= maxCoveringZoom);
if (tile.tileID.overscaledZ === topmostZoom) { if (!candidates.length) {
retain[tile.tileID.key] = tile.tileID; incomplete[targetID.key] = targetID;
} continue;
} }
// retain the uppermost descendents in the topmost zoom below the target tile
const topZoom = Math.min(...candidates.map(t => t.tileID.overscaledZ));
const topIDs = candidates.filter(t => t.tileID.overscaledZ === topZoom).map(t => t.tileID);
for (const tileID of topIDs) {
retain[tileID.key] = tileID;
}
//determine if the retained generation is fully covered
if (!this._areDescendentsComplete(topIDs, topZoom, targetID.overscaledZ)) {
incomplete[targetID.key] = targetID;
} }
} }
return incomplete;
} }
/** /**
* Return dictionary of qualified loaded descendents for each provided target tile id * Return dictionary of qualified loaded descendents for each provided target tile id
*/ */
_getLoadedDescendents(targetTileIDs: OverscaledTileID[]) { _getLoadedDescendents(targetTileIDs: OverscaledTileID[]) {
const loadedDescendents: { [_: string]: Tile[] } = {}; const loadedDescendents: Record<string, Tile[]> = {};
// enumerate tiles currently in this source and find the loaded descendents of each target tile // enumerate tiles currently in this source and find the loaded descendents of each target tile
for (const sourceKey in this._tiles) { for (const sourceKey in this._tiles) {
@ -435,42 +440,30 @@ export class SourceCache extends Evented {
} }
/** /**
* Find a loaded parent of the given tile (up to minCoveringZoom) * Determine if tile ids fully cover the current generation.
* - 1st generation: need 4 children or 1 overscaled child
* - 2nd generation: need 16 children or 1 overscaled child
*/ */
findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): Tile { _areDescendentsComplete(generationIDs: OverscaledTileID[], generationZ: number, ancestorZ: number) {
if (tileID.key in this._loadedParentTiles) { //if overscaled, seeking 1 tile at generationZ, otherwise seeking a power of 4 for each descending Z
const parent = this._loadedParentTiles[tileID.key]; if (generationIDs.length === 1 && generationIDs[0].isOverscaled()) {
if (parent && parent.tileID.overscaledZ >= minCoveringZoom) { return generationIDs[0].overscaledZ === generationZ;
return parent; } else {
} else { const expectedTiles = Math.pow(4, generationZ - ancestorZ); //4, 16, 64 (for first 3 gens)
return null; return expectedTiles === generationIDs.length;
}
}
for (let z = tileID.overscaledZ - 1; z >= minCoveringZoom; z--) {
const parentTileID = tileID.scaledTo(z);
const tile = this._getLoadedTile(parentTileID);
if (tile) {
return tile;
}
} }
} }
/** /**
* Find a loaded sibling of the given tile * Get a loaded tile currently in this source.
* - loaded tiles exist in this._tiles - a cached tile is not a loaded tile
*/ */
findLoadedSibling(tileID: OverscaledTileID): Tile { _getLoadedTile(tileID: OverscaledTileID): Tile | null {
// If a tile with this ID already exists, return it
return this._getLoadedTile(tileID);
}
_getLoadedTile(tileID: OverscaledTileID): Tile {
const tile = this._tiles[tileID.key]; const tile = this._tiles[tileID.key];
if (tile && tile.hasData()) { if (tile?.hasData()) {
return tile; return tile;
} }
// TileCache ignores wrap in lookup. return null;
const cachedTile = this._cache.getByKey(tileID.wrapped().key);
return cachedTile;
} }
/** /**
@ -517,7 +510,7 @@ export class SourceCache extends Evented {
this._prevLng = lng; this._prevLng = lng;
if (wrapDelta) { if (wrapDelta) {
const tiles: {[_: string]: Tile} = {}; const tiles: Record<string, Tile> = {};
for (const key in this._tiles) { for (const key in this._tiles) {
const tile = this._tiles[key]; const tile = this._tiles[key];
tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta);
@ -525,102 +518,7 @@ export class SourceCache extends Evented {
} }
this._tiles = tiles; this._tiles = tiles;
// Reset tile reload timers this._resetTileReloadTimers();
for (const id in this._timers) {
clearTimeout(this._timers[id]);
delete this._timers[id];
}
for (const id in this._tiles) {
const tile = this._tiles[id];
this._setTileReloadTimer(id, tile);
}
}
}
_updateCoveredAndRetainedTiles(
retain: { [_: string]: OverscaledTileID },
minCoveringZoom: number,
idealTileIDs: OverscaledTileID[],
terrain?: Terrain
) {
const tilesForFading: { [_: string]: OverscaledTileID } = {};
const fadingTiles = {};
const ids = Object.keys(retain);
const now = browser.now();
for (const id of ids) {
const tileID = retain[id];
const tile = this._tiles[id];
// when fadeEndTime is 0, the tile is created but registerFadeDuration
// has not been called, therefore must be kept in fadingTiles dictionary
// for next round of rendering
if (!tile || (tile.fadeEndTime !== 0 && tile.fadeEndTime <= now)) {
continue;
}
// if the tile is loaded but still fading in, find parents to cross-fade with it
const parentTile = this.findLoadedParent(tileID, minCoveringZoom);
const siblingTile = this.findLoadedSibling(tileID);
const fadeTileRef = parentTile || siblingTile || null;
if (fadeTileRef) {
this._addTile(fadeTileRef.tileID);
tilesForFading[fadeTileRef.tileID.key] = fadeTileRef.tileID;
}
fadingTiles[id] = tileID;
}
// for tiles that are still fading in, also find children to cross-fade with
this._retainLoadedChildren(fadingTiles, retain);
for (const id in tilesForFading) {
if (!retain[id]) {
// If a tile is only needed for fading, mark it as covered so that it isn't rendered on it's own.
this._coveredTiles[id] = true;
retain[id] = tilesForFading[id];
}
}
// disable fading logic in terrain3D mode to avoid rendering two tiles on the same place
if (terrain) {
const idealRasterTileIDs: { [_: string]: OverscaledTileID } = {};
const missingTileIDs: { [_: string]: OverscaledTileID } = {};
for (const tileID of idealTileIDs) {
if (this._tiles[tileID.key].hasData())
idealRasterTileIDs[tileID.key] = tileID;
else
missingTileIDs[tileID.key] = tileID;
}
// search for a complete set of children for each missing tile
for (const key in missingTileIDs) {
const children = missingTileIDs[key].children(this._source.maxzoom);
if (this._tiles[children[0].key] && this._tiles[children[1].key] && this._tiles[children[2].key] && this._tiles[children[3].key]) {
idealRasterTileIDs[children[0].key] = retain[children[0].key] = children[0];
idealRasterTileIDs[children[1].key] = retain[children[1].key] = children[1];
idealRasterTileIDs[children[2].key] = retain[children[2].key] = children[2];
idealRasterTileIDs[children[3].key] = retain[children[3].key] = children[3];
delete missingTileIDs[key];
}
}
// search for parent or sibling for each missing tile
for (const key in missingTileIDs) {
const tileID = missingTileIDs[key];
const parentTile = this.findLoadedParent(tileID, this._source.minzoom);
const siblingTile = this.findLoadedSibling(tileID);
const fadeTileRef = parentTile || siblingTile || null;
if (fadeTileRef) {
idealRasterTileIDs[fadeTileRef.tileID.key] = retain[fadeTileRef.tileID.key] = fadeTileRef.tileID;
// remove idealTiles which would be rendered twice
for (const key in idealRasterTileIDs) {
if (idealRasterTileIDs[key].isChildOf(fadeTileRef.tileID)) delete idealRasterTileIDs[key];
}
}
}
// cover all tiles which are not needed
for (const key in this._tiles) {
if (!idealRasterTileIDs[key]) this._coveredTiles[key] = true;
}
} }
} }
@ -638,10 +536,6 @@ export class SourceCache extends Evented {
this.updateCacheSize(transform); this.updateCacheSize(transform);
this.handleWrapJump(this.transform.center.lng); this.handleWrapJump(this.transform.center.lng);
// Covered is a list of retained tiles who's areas are fully covered by other,
// better, retained tiles. They are not drawn separately.
this._coveredTiles = {};
let idealTileIDs: OverscaledTileID[]; let idealTileIDs: OverscaledTileID[];
if (!this.used && !this.usedForTerrain) { if (!this.used && !this.usedForTerrain) {
@ -661,27 +555,13 @@ export class SourceCache extends Evented {
}); });
if (this._source.hasTile) { if (this._source.hasTile) {
idealTileIDs = idealTileIDs.filter((coord) => (this._source.hasTile as any)(coord)); idealTileIDs = idealTileIDs.filter((coord) => this._source.hasTile(coord));
} }
} }
// Determine the overzooming/underzooming amounts. // When sourcecache is used for terrain also load parent tiles for complete rendering of 3d terrain levels
const zoom = coveringZoomLevel(transform, this._source);
const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom);
// When sourcecache is used for terrain also load parent tiles to avoid flickering when zooming out
if (this.usedForTerrain) { if (this.usedForTerrain) {
const parents = {}; idealTileIDs = this._addTerrainIdealTiles(idealTileIDs);
for (const tileID of idealTileIDs) {
if (tileID.canonical.z > this._source.minzoom) {
const parent = tileID.scaledTo(tileID.canonical.z - 1);
parents[parent.key] = parent;
// load very low zoom to calculate tile visibility in transform.coveringTiles and high zoomlevels correct
const parent2 = tileID.scaledTo(Math.max(this._source.minzoom, Math.min(tileID.canonical.z, 5)));
parents[parent2.key] = parent2;
}
}
idealTileIDs = idealTileIDs.concat(Object.values(parents));
} }
const noPendingDataEmissions = idealTileIDs.length === 0 && !this._updated && this._didEmitContent; const noPendingDataEmissions = idealTileIDs.length === 0 && !this._updated && this._didEmitContent;
@ -695,37 +575,84 @@ export class SourceCache extends Evented {
// Retain is a list of tiles that we shouldn't delete, even if they are not // Retain is a list of tiles that we shouldn't delete, even if they are not
// the most ideal tile for the current viewport. This may include tiles like // the most ideal tile for the current viewport. This may include tiles like
// parent or child tiles that are *already* loaded. // parent or child tiles that are *already* loaded.
const retain = this._updateRetainedTiles(idealTileIDs, zoom); const zoom: number = coveringZoomLevel(transform, this._source);
const retain: Record<string, OverscaledTileID> = this._updateRetainedTiles(idealTileIDs, zoom);
if (isRasterType(this._source.type)) { // enable fading for raster source except when using terrain which doesn't currently support fading
this._updateCoveredAndRetainedTiles(retain, minCoveringZoom, idealTileIDs, terrain); const isRaster = isRasterType(this._source.type);
if (isRaster && this._rasterFadeDuration > 0 && !terrain) {
this._updateFadingTiles(idealTileIDs, retain);
} }
for (const retainedId in retain) { // clean up non-retained tiles in this source
// Make sure retained tiles always clear any existing fade holds if (isRaster) {
// so that if they're removed again their fade timer starts fresh. this._cleanUpRasterTiles(retain);
this._tiles[retainedId].clearFadeHold(); } else {
this._cleanUpVectorTiles(retain);
} }
}
// Remove the tiles we don't need anymore. /**
const remove = keysDifference(this._tiles, retain); * Remove raster tiles that are no longer retained
for (const tileID of remove) { */
const tile = this._tiles[tileID]; _cleanUpRasterTiles(retain: Record<string, OverscaledTileID>) {
if (tile.hasSymbolBuckets && !tile.holdingForFade()) { for (const key in this._tiles) {
tile.setHoldDuration(this.map._fadeDuration); if (!retain[key]) {
} else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { this._removeTile(key);
this._removeTile(tileID); }
}
}
/**
* Remove vector tiles that are no longer retained and also not needed for symbol fading
*/
_cleanUpVectorTiles(retain: Record<string, OverscaledTileID>) {
for (const key in this._tiles) {
const tile = this._tiles[key];
// retained - clear fade hold so if it's removed again fade timer starts fresh.
if (retain[key]) {
tile.clearSymbolFadeHold();
continue;
}
// remove non-retained tiles without symbols
if (!tile.hasSymbolBuckets) {
this._removeTile(key);
continue;
}
// for tile with symbols - hold for fade - then remove
if (!tile.holdingForSymbolFade()) {
tile.setSymbolHoldDuration(this.map._fadeDuration);
} else if (tile.symbolFadeFinished()) {
this._removeTile(key);
}
}
}
/**
* Add ideal tiles needed for 3D terrain rendering
*/
_addTerrainIdealTiles(idealTileIDs: OverscaledTileID[]): OverscaledTileID[] {
const ancestors = [];
for (const tileID of idealTileIDs) {
if (tileID.canonical.z > this._source.minzoom) {
const parent = tileID.scaledTo(tileID.canonical.z - 1);
ancestors.push(parent);
// load very low zoom to calculate tile visibility in transform.coveringTiles and high zoom levels correct
const parent2 = tileID.scaledTo(Math.max(this._source.minzoom, Math.min(tileID.canonical.z, 5)));
ancestors.push(parent2);
} }
} }
// Construct caches of loaded parents & siblings return idealTileIDs.concat(ancestors);
this._updateLoadedParentTileCache();
this._updateLoadedSiblingTileCache();
} }
releaseSymbolFadeTiles() { releaseSymbolFadeTiles() {
for (const id in this._tiles) { for (const id in this._tiles) {
if (this._tiles[id].holdingForFade()) { if (this._tiles[id].holdingForSymbolFade()) {
this._removeTile(id); this._removeTile(id);
} }
} }
@ -736,64 +663,34 @@ export class SourceCache extends Evented {
* children so they can be displayed as substitutes pending load of each ideal tile (to reduce flickering). * children so they can be displayed as substitutes pending load of each ideal tile (to reduce flickering).
* If no loaded children are available, fallback to seeking loaded parents as an alternative substitute. * If no loaded children are available, fallback to seeking loaded parents as an alternative substitute.
*/ */
_updateRetainedTiles(idealTileIDs: Array<OverscaledTileID>, zoom: number): {[_: string]: OverscaledTileID} { _updateRetainedTiles(idealTileIDs: Array<OverscaledTileID>, zoom: number): Record<string, OverscaledTileID> {
const retain: {[_: string]: OverscaledTileID} = {}; const retain: Record<string, OverscaledTileID> = {};
const checked: {[_: string]: boolean} = {}; const checked: Record<string, boolean> = {};
const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom); const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom);
const missingTiles = {}; let missingIdealTiles = {};
for (const tileID of idealTileIDs) { for (const idealID of idealTileIDs) {
const tile = this._addTile(tileID); const idealTile = this._addTile(idealID);
// retain the tile even if it's not loaded because it's an ideal tile. // retain the tile even if it's not loaded because it's an ideal tile.
retain[tileID.key] = tileID; retain[idealID.key] = idealID;
if (tile.hasData()) continue; if (!idealTile.hasData()) {
missingIdealTiles[idealID.key] = idealID;
if (zoom < this._source.maxzoom) {
// save missing tiles that potentially have loaded children
missingTiles[tileID.key] = tileID;
} }
} }
this._retainLoadedChildren(missingTiles, retain); missingIdealTiles = this._retainLoadedChildren(missingIdealTiles, retain);
for (const tileID of idealTileIDs) { // for remaining missing tiles with incomplete child coverage, seek a loaded parent tile
let tile = this._tiles[tileID.key]; for (const idealKey in missingIdealTiles) {
const tileID = missingIdealTiles[idealKey];
if (tile.hasData()) continue; let tile = this._tiles[idealKey];
// The tile we require is not yet loaded or does not exist;
// Attempt to find children that fully cover it.
if (zoom + 1 > this._source.maxzoom) {
// We're looking for an overzoomed child tile.
const childCoord = tileID.children(this._source.maxzoom)[0];
const childTile = this.getTile(childCoord);
if (!!childTile && childTile.hasData()) {
retain[childCoord.key] = childCoord;
continue; // tile is covered by overzoomed child
}
} else {
// check if all 4 immediate children are loaded (i.e. the missing ideal tile is covered)
const children = tileID.children(this._source.maxzoom);
if (children.length === 4 &&
retain[children[0].key] &&
retain[children[1].key] &&
retain[children[2].key] &&
retain[children[3].key]) continue; // tile is covered by children
if (children.length === 1 &&
retain[children[0].key]) continue; // tile is covered by overscaled child
}
// We couldn't find child tiles that entirely cover the ideal tile; look for parents now.
// As we ascend up the tile pyramid of the ideal tile, we check whether the parent // As we ascend up the tile pyramid of the ideal tile, we check whether the parent
// tile has been previously requested (and errored because we only loop over tiles with no data) // tile has been previously requested (and errored because we only loop over tiles with no data)
// in order to determine if we need to request its parent. // in order to determine if we need to request its parent.
let parentWasRequested = tile.wasRequested(); let parentWasRequested = tile?.wasRequested();
for (let overscaledZ = tileID.overscaledZ - 1; overscaledZ >= minCoveringZoom; --overscaledZ) { for (let overscaledZ = tileID.overscaledZ - 1; overscaledZ >= minCoveringZoom; --overscaledZ) {
const parentId = tileID.scaledTo(overscaledZ); const parentId = tileID.scaledTo(overscaledZ);
@ -822,59 +719,181 @@ export class SourceCache extends Evented {
return retain; return retain;
} }
_updateLoadedParentTileCache() { /**
this._loadedParentTiles = {}; * Designate fading bases and parents using a many-to-one relationship where the lower children fade in/out
* with their parents. Raster shaders are not currently designed for a one-to-many fade relationship.
*
* Tiles that are candidates for fading out must be loaded and rendered tiles, as loading a tile to then
* fade it out would not appear smoothly. The first source of truth for tile fading always starts at the
* ideal tile, which continually changes on map adjustment. The state of the previously rendered ideal
* tile plane indicates which direction to fade each part of the newer ideal plane (with varying z).
*
* For a pitched map, the back of the map can have decreasing zooms while the front can have increasing zooms.
* Fade logic must therefore adapt dynamically based on the previously rendered ideal tile set.
*/
_updateFadingTiles(idealTileIDs: OverscaledTileID[], retain: Record<string, OverscaledTileID>) {
const currentTime: number = now();
const edgeTileIDs: Set<OverscaledTileID> = getEdgeTiles(idealTileIDs);
for (const tileKey in this._tiles) { for (const idealID of idealTileIDs) {
const path = []; const idealTile = this._tiles[idealID.key];
let parentTile: Tile;
let currentId = this._tiles[tileKey].tileID;
// Find the closest loaded ancestor by traversing the tile tree towards the root and // reset any previously departing(ed) tiles that are now ideal tiles
// caching results along the way if (idealTile.fadingDirection === FadingDirections.Departing || idealTile.fadeOpacity === 0) {
while (currentId.overscaledZ > 0) { idealTile.resetFadeLogic();
// Do we have a cached result from previous traversals?
if (currentId.key in this._loadedParentTiles) {
parentTile = this._loadedParentTiles[currentId.key];
break;
}
path.push(currentId.key);
// Is the parent loaded?
const parentId = currentId.scaledTo(currentId.overscaledZ - 1);
parentTile = this._getLoadedTile(parentId);
if (parentTile) {
break;
}
currentId = parentId;
} }
// Cache the result of this traversal to all newly visited tiles const parentIsFader = this._updateFadingAncestor(idealTile, retain, currentTime);
for (const key of path) { if (parentIsFader) continue;
this._loadedParentTiles[key] = parentTile;
} const childIsFader = this._updateFadingDescendents(idealTile, retain, currentTime);
if (childIsFader) continue;
const edgeIsFader = this._updateFadingEdge(idealTile, edgeTileIDs, currentTime);
if (edgeIsFader) continue;
// for all remaining non-fading ideal tiles reset the fade logic
idealTile.resetFadeLogic();
} }
} }
/** /**
* Update the cache of loaded sibling tiles * Many-to-one cross-fade. Set 4 ideal tiles as the fading base for a rendered parent tile
* as the fading parent. Here the parent is fading out and the ideal tile is fading in.
* *
* Sibling tiles are tiles that share the same zoom level and * Parent tile - fading out -- Fading Parent
* x/y position but have different wrap values *
* Maintaining sibling tile cache allows fading from old to new tiles * Ideal tiles - fading in -- Base Role = Incoming
* of the same position and zoom level *
*
*/ */
_updateLoadedSiblingTileCache() { _updateFadingAncestor(idealTile: Tile, retain: Record<string, OverscaledTileID>, now: number): boolean {
this._loadedSiblingTiles = {}; if (!idealTile.hasData()) return false;
for (const tileKey in this._tiles) { const {tileID: idealID, fadingRole, fadingDirection, fadingParentID} = idealTile;
const currentId = this._tiles[tileKey].tileID; // ideal tile already has fading parent - retain and return
const siblingTile: Tile = this._getLoadedTile(currentId); if (fadingRole === FadingRoles.Base && fadingDirection === FadingDirections.Incoming && fadingParentID) {
this._loadedSiblingTiles[currentId.key] = siblingTile; retain[fadingParentID.key] = fadingParentID;
return true;
} }
// find a loaded parent tile to fade with the ideal tile
const minAncestorZ = Math.max(idealID.overscaledZ - this._maxFadingAncestorLevels, this._source.minzoom);
for (let ancestorZ = idealID.overscaledZ - 1; ancestorZ >= minAncestorZ; ancestorZ--) {
const ancestorID = idealID.scaledTo(ancestorZ);
const ancestorTile = this._getLoadedTile(ancestorID);
if (!ancestorTile) continue;
// ideal tile (base) is fading in
idealTile.setCrossFadeLogic({
fadingRole: FadingRoles.Base,
fadingDirection: FadingDirections.Incoming,
fadingParentID: ancestorTile.tileID, // fading out
fadeEndTime: now + this._rasterFadeDuration
});
// ancestor tile (parent) is fading out
ancestorTile.setCrossFadeLogic({
fadingRole: FadingRoles.Parent,
fadingDirection: FadingDirections.Departing,
fadeEndTime: now + this._rasterFadeDuration
});
retain[ancestorID.key] = ancestorID;
return true;
}
return false;
}
/**
* Many-to-one cross-fade. Search descendents of ideal tiles as the fading base with the ideal tile
* as the fading parent. Here the children are fading out and the ideal tile is fading in.
*
*
*
* Ideal tiles - fading in -- Fading Parent
*
* Child tiles - fading out -- Base Role = Departing
*
* Try direct children first. If none found, try grandchildren. Stops at the first generation that provides a fader.
*/
_updateFadingDescendents(idealTile: Tile, retain: Record<string, OverscaledTileID>, now: number): boolean {
if (!idealTile.hasData()) return false;
// search first level of descendents (4 tiles)
const idealChildren = idealTile.tileID.children(this._source.maxzoom);
let hasFader = this._updateFadingChildren(idealTile, idealChildren, retain, now);
if (hasFader) return true;
// search second level of descendents (16 tiles)
for (const childID of idealChildren) {
const grandChildIDs = childID.children(this._source.maxzoom);
if (this._updateFadingChildren(idealTile, grandChildIDs, retain, now)) {
hasFader = true;
}
}
return hasFader;
}
_updateFadingChildren(idealTile: Tile, childIDs: OverscaledTileID[], retain: Record<string, OverscaledTileID>, now: number): boolean {
if (childIDs[0].overscaledZ >= this._source.maxzoom) return false;
let foundFader = false;
// find loaded child tiles to fade with the ideal tile
for (const childID of childIDs) {
const childTile = this._getLoadedTile(childID);
if (!childTile) continue;
const {fadingRole, fadingDirection, fadingParentID} = childTile;
if (fadingRole !== FadingRoles.Base || fadingDirection !== FadingDirections.Departing || !fadingParentID) {
// child tile (base) is fading out
childTile.setCrossFadeLogic({
fadingRole: FadingRoles.Base,
fadingDirection: FadingDirections.Departing,
fadingParentID: idealTile.tileID,
fadeEndTime: now + this._rasterFadeDuration
});
// ideal tile (parent) is fading in
idealTile.setCrossFadeLogic({
fadingRole: FadingRoles.Parent,
fadingDirection: FadingDirections.Incoming,
fadeEndTime: now + this._rasterFadeDuration
});
}
retain[childID.key] = childID;
foundFader = true;
}
return foundFader;
}
/**
* One-to-one self fading for unloaded edge tiles (for panning sideways on map). for loading tiles over gaps it feels
* more natural for them to fade in, however if they are already loaded/cached then there is no need to fade as map will
* look cohesive with no gaps. Note that draw_raster determines fade priority, as many-to-one fade supersedes edge fading.
*/
_updateFadingEdge(idealTile: Tile, edgeTileIDs: Set<OverscaledTileID>, now: number): boolean {
const idealID: OverscaledTileID = idealTile.tileID;
// tile is already self fading
if (idealTile.selfFading) {
return true;
}
// fading not needed for tiles that are already loaded
if (idealTile.hasData()) {
return false;
}
// enable fading for loading edges with no data
if (edgeTileIDs.has(idealID)) {
const fadeEndTime = now + this._rasterFadeDuration;
idealTile.setSelfFadeLogic(fadeEndTime);
return true;
}
return false;
} }
/** /**
@ -887,15 +906,15 @@ export class SourceCache extends Evented {
tile = this._cache.getAndRemove(tileID); tile = this._cache.getAndRemove(tileID);
if (tile) { if (tile) {
//reset fading logic to remove stale fading data from cache
tile.resetFadeLogic();
// set timer for the reloading of the tile upon expiration
this._setTileReloadTimer(tileID.key, tile); this._setTileReloadTimer(tileID.key, tile);
// set the tileID because the cached tile could have had a different wrap value // set the tileID because the cached tile could have had a different wrap value
tile.tileID = tileID; tile.tileID = tileID;
this._state.initializeTileState(tile, this.map ? this.map.painter : null); this._state.initializeTileState(tile, this.map ? this.map.painter : null);
if (this._cacheTimers[tileID.key]) {
clearTimeout(this._cacheTimers[tileID.key]);
delete this._cacheTimers[tileID.key];
this._setTileReloadTimer(tileID.key, tile);
}
} }
const cached = tile; const cached = tile;
@ -914,18 +933,38 @@ export class SourceCache extends Evented {
return tile; return tile;
} }
/**
* Set a timeout to reload the tile after it expires
*/
_setTileReloadTimer(id: string, tile: Tile) { _setTileReloadTimer(id: string, tile: Tile) {
if (id in this._timers) { this._clearTileReloadTimer(id);
clearTimeout(this._timers[id]);
delete this._timers[id];
}
const expiryTimeout = tile.getExpiryTimeout(); const expiryTimeout = tile.getExpiryTimeout();
if (expiryTimeout) { if (expiryTimeout) {
this._timers[id] = setTimeout(() => { const reload = () => {
this._reloadTile(id, 'expired'); this._reloadTile(id, 'expired');
delete this._timers[id]; delete this._timers[id];
}, expiryTimeout); };
this._timers[id] = setTimeout(reload, expiryTimeout);
}
}
_clearTileReloadTimer(id: string) {
const timeout = this._timers[id];
if (timeout) {
clearTimeout(timeout);
delete this._timers[id];
}
}
_resetTileReloadTimers() {
for (const id in this._timers) {
clearTimeout(this._timers[id]);
delete this._timers[id];
}
for (const id in this._tiles) {
const tile = this._tiles[id];
this._setTileReloadTimer(id, tile);
} }
} }
@ -953,10 +992,7 @@ export class SourceCache extends Evented {
tile.uses--; tile.uses--;
delete this._tiles[id]; delete this._tiles[id];
if (this._timers[id]) { this._clearTileReloadTimer(id);
clearTimeout(this._timers[id]);
delete this._timers[id];
}
if (tile.uses > 0) if (tile.uses > 0)
return; return;
@ -970,24 +1006,28 @@ export class SourceCache extends Evented {
} }
} }
/** @internal */ /** @internal
* Handles incoming source data messages (i.e. after the source has been updated via a worker that has fired
* to map.ts data event). For sources with mutable data, the 'content' event fires when the underlying data
* to a source has changed. (i.e. GeoJSONSource.setData and ImageSource.setCoordinates)
*/
private _dataHandler(e: MapSourceDataEvent) { private _dataHandler(e: MapSourceDataEvent) {
if (e.dataType !== 'source') return;
const eventSourceDataType = e.sourceDataType; if (e.sourceDataType === 'metadata') {
if (e.dataType === 'source' && eventSourceDataType === 'metadata') {
this._sourceLoaded = true; this._sourceLoaded = true;
return;
} }
// for sources with mutable data, this event fires when the underlying data if (e.sourceDataType !== 'content' || !this._sourceLoaded || this._paused) {
// to a source is changed. (i.e. GeoJSONSource.setData and ImageSource.serCoordinates) return;
if (this._sourceLoaded && !this._paused && e.dataType === 'source' && eventSourceDataType === 'content') {
this.reload(e.sourceDataChanged);
if (this.transform) {
this.update(this.transform, this.terrain);
}
this._didEmitContent = true;
} }
this.reload(e.sourceDataChanged);
if (this.transform) {
this.update(this.transform, this.terrain);
}
this._didEmitContent = true;
} }
/** /**
@ -1030,7 +1070,7 @@ export class SourceCache extends Evented {
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
const tile = this._tiles[ids[i]]; const tile = this._tiles[ids[i]];
if (tile.holdingForFade()) { if (tile.holdingForSymbolFade()) {
// Tiles held for fading are covered by tiles that are closer to ideal // Tiles held for fading are covered by tiles that are closer to ideal
continue; continue;
} }
@ -1098,11 +1138,11 @@ export class SourceCache extends Evented {
return true; return true;
} }
if (isRasterType(this._source.type)) { if (isRasterType(this._source.type) && this._rasterFadeDuration > 0) {
const now = browser.now(); const currentTime = now();
for (const id in this._tiles) { for (const id in this._tiles) {
const tile = this._tiles[id]; const tile = this._tiles[id];
if (tile.fadeEndTime >= now) { if (tile.fadeEndTime >= currentTime) {
return true; return true;
} }
} }
@ -1111,6 +1151,10 @@ export class SourceCache extends Evented {
return false; return false;
} }
setRasterFadeDuration(fadeDuration: number) {
this._rasterFadeDuration = fadeDuration;
}
/** /**
* Set the value of a particular state for a feature * Set the value of a particular state for a feature
*/ */

View File

@ -6,7 +6,7 @@ import {Evented} from '../util/evented';
import type {ITransform} from '../geo/transform_interface'; import type {ITransform} from '../geo/transform_interface';
import type {SourceCache} from '../source/source_cache'; import type {SourceCache} from '../source/source_cache';
import {type Terrain} from '../render/terrain'; import {type Terrain} from '../render/terrain';
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {coveringTiles} from '../geo/projection/covering_tiles'; import {coveringTiles} from '../geo/projection/covering_tiles';
import {createMat4f64} from '../util/util'; import {createMat4f64} from '../util/util';
import {type CanonicalTileRange} from './image_source'; import {type CanonicalTileRange} from './image_source';
@ -57,7 +57,7 @@ export class TerrainSourceCache extends Evented {
/** /**
* used to determine whether depth & coord framebuffers need updating * used to determine whether depth & coord framebuffers need updating
*/ */
_lastTilesetChange: number = browser.now(); _lastTilesetChange: number = now();
constructor(sourceCache: SourceCache) { constructor(sourceCache: SourceCache) {
super(); super();
@ -103,7 +103,7 @@ export class TerrainSourceCache extends Evented {
tileID.terrainRttPosMatrix32f = new Float64Array(16) as any; tileID.terrainRttPosMatrix32f = new Float64Array(16) as any;
mat4.ortho(tileID.terrainRttPosMatrix32f, 0, EXTENT, EXTENT, 0, 0, 1); mat4.ortho(tileID.terrainRttPosMatrix32f, 0, EXTENT, EXTENT, 0, 0, 1);
this._tiles[tileID.key] = new Tile(tileID, this.tileSize); this._tiles[tileID.key] = new Tile(tileID, this.tileSize);
this._lastTilesetChange = browser.now(); this._lastTilesetChange = now();
} }
} }
// free unused tiles // free unused tiles

View File

@ -6,7 +6,7 @@ import {featureFilter} from '@maplibre/maplibre-gl-style-spec';
import {SymbolBucket} from '../data/bucket/symbol_bucket'; import {SymbolBucket} from '../data/bucket/symbol_bucket';
import {CollisionBoxArray} from '../data/array_types.g'; import {CollisionBoxArray} from '../data/array_types.g';
import {Texture} from '../render/texture'; import {Texture} from '../render/texture';
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {toEvaluationFeature} from '../data/evaluation_feature'; import {toEvaluationFeature} from '../data/evaluation_feature';
import {EvaluationParameters} from '../style/evaluation_parameters'; import {EvaluationParameters} from '../style/evaluation_parameters';
import {type SourceFeatureState} from '../source/source_state'; import {type SourceFeatureState} from '../source/source_state';
@ -33,6 +33,7 @@ import type {VectorTileLayer} from '@mapbox/vector-tile';
import type {ExpiryData} from '../util/ajax'; import type {ExpiryData} from '../util/ajax';
import type {QueryRenderedFeaturesOptionsStrict, QuerySourceFeatureOptionsStrict} from './query_features'; import type {QueryRenderedFeaturesOptionsStrict, QuerySourceFeatureOptionsStrict} from './query_features';
import type {FeatureIndex, QueryResults} from '../data/feature_index'; import type {FeatureIndex, QueryResults} from '../data/feature_index';
import type {DashEntry} from '../render/line_atlas';
/** /**
* The tile's state, can be: * The tile's state, can be:
* *
@ -45,6 +46,21 @@ import type {FeatureIndex, QueryResults} from '../data/feature_index';
*/ */
export type TileState = 'loading' | 'loaded' | 'reloading' | 'unloaded' | 'errored' | 'expired'; export type TileState = 'loading' | 'loaded' | 'reloading' | 'unloaded' | 'errored' | 'expired';
/** @internal */
type CrossFadeArgs = {
fadingRole: FadingRoles;
fadingDirection: FadingDirections;
fadingParentID?: OverscaledTileID;
fadeEndTime: number;
};
export enum FadingRoles {
Base, Parent
}
export enum FadingDirections {
Departing, Incoming
}
/** /**
* A tile object is the combination of a Coordinate, which defines * A tile object is the combination of a Coordinate, which defines
* its place, as well as a unique ID and data tracking for its content * its place, as well as a unique ID and data tracking for its content
@ -59,13 +75,19 @@ export class Tile {
latestRawTileData: ArrayBuffer; latestRawTileData: ArrayBuffer;
imageAtlas: ImageAtlas; imageAtlas: ImageAtlas;
imageAtlasTexture: Texture; imageAtlasTexture: Texture;
dashPositions: {[_: string]: DashEntry};
glyphAtlasImage: AlphaImage; glyphAtlasImage: AlphaImage;
glyphAtlasTexture: Texture; glyphAtlasTexture: Texture;
expirationTime: any; expirationTime: any;
expiredRequestCount: number; expiredRequestCount: number;
state: TileState; state: TileState;
fadingRole: FadingRoles;
fadingDirection: FadingDirections;
fadingParentID: OverscaledTileID;
selfFading: boolean;
timeAdded: number = 0; timeAdded: number = 0;
fadeEndTime: number = 0; fadeEndTime: number = 0;
fadeOpacity: number = 1;
collisionBoxArray: CollisionBoxArray; collisionBoxArray: CollisionBoxArray;
redoWhenDone: boolean; redoWhenDone: boolean;
showCollisionBoxes: boolean; showCollisionBoxes: boolean;
@ -122,16 +144,47 @@ export class Tile {
this.state = 'loading'; this.state = 'loading';
} }
registerFadeDuration(duration: number) { isRenderable(symbolLayer: boolean): boolean {
const fadeEndTime = duration + this.timeAdded; return (
this.hasData() &&
(!this.fadeEndTime || this.fadeOpacity > 0) && // raster fading
(symbolLayer || !this.holdingForSymbolFade()) // symbol fading
);
}
if (fadeEndTime < this.fadeEndTime) { /**
return; * @internal
} * Many-to-one crossfade between a base tile and parent/ancestor tile (when zooming)
*/
setCrossFadeLogic({fadingRole, fadingDirection, fadingParentID, fadeEndTime}: CrossFadeArgs) {
this.resetFadeLogic();
this.fadingRole = fadingRole;
this.fadingDirection = fadingDirection;
this.fadingParentID = fadingParentID;
this.fadeEndTime = fadeEndTime; this.fadeEndTime = fadeEndTime;
} }
/**
* Self fading for edge tiles (when panning map)
*/
setSelfFadeLogic(fadeEndTime: number) {
this.resetFadeLogic();
this.selfFading = true;
this.fadeEndTime = fadeEndTime;
}
resetFadeLogic() {
this.fadingRole = null;
this.fadingDirection = null;
this.fadingParentID = null;
this.selfFading = false;
this.timeAdded = now();
this.fadeEndTime = 0;
this.fadeOpacity = 1;
}
wasRequested() { wasRequested() {
return this.state === 'errored' || this.state === 'loaded' || this.state === 'reloading'; return this.state === 'errored' || this.state === 'loaded' || this.state === 'reloading';
} }
@ -218,6 +271,7 @@ export class Tile {
if (data.glyphAtlasImage) { if (data.glyphAtlasImage) {
this.glyphAtlasImage = data.glyphAtlasImage; this.glyphAtlasImage = data.glyphAtlasImage;
} }
this.dashPositions = data.dashPositions;
} }
/** /**
@ -241,6 +295,10 @@ export class Tile {
this.glyphAtlasTexture.destroy(); this.glyphAtlasTexture.destroy();
} }
if (this.dashPositions) {
this.dashPositions = null;
}
this.latestFeatureIndex = null; this.latestFeatureIndex = null;
this.state = 'unloaded'; this.state = 'unloaded';
} }
@ -423,7 +481,7 @@ export class Tile {
const sourceLayerStates = states[sourceLayerId]; const sourceLayerStates = states[sourceLayerId];
if (!sourceLayer || !sourceLayerStates || Object.keys(sourceLayerStates).length === 0) continue; if (!sourceLayer || !sourceLayerStates || Object.keys(sourceLayerStates).length === 0) continue;
bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas && this.imageAtlas.patternPositions || {}); bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas && this.imageAtlas.patternPositions || {}, this.dashPositions || {});
const layer = painter && painter.style && painter.style.getLayer(id); const layer = painter && painter.style && painter.style.getLayer(id);
if (layer) { if (layer) {
this.queryPadding = Math.max(this.queryPadding, layer.queryRadius(bucket)); this.queryPadding = Math.max(this.queryPadding, layer.queryRadius(bucket));
@ -431,20 +489,20 @@ export class Tile {
} }
} }
holdingForFade(): boolean { holdingForSymbolFade(): boolean {
return this.symbolFadeHoldUntil !== undefined; return this.symbolFadeHoldUntil !== undefined;
} }
symbolFadeFinished(): boolean { symbolFadeFinished(): boolean {
return !this.symbolFadeHoldUntil || this.symbolFadeHoldUntil < browser.now(); return !this.symbolFadeHoldUntil || this.symbolFadeHoldUntil < now();
} }
clearFadeHold() { clearSymbolFadeHold() {
this.symbolFadeHoldUntil = undefined; this.symbolFadeHoldUntil = undefined;
} }
setHoldDuration(duration: number) { setSymbolHoldDuration(duration: number) {
this.symbolFadeHoldUntil = browser.now() + duration; this.symbolFadeHoldUntil = now() + duration;
} }
setDependencies(namespace: string, dependencies: Array<string>) { setDependencies(namespace: string, dependencies: Array<string>) {

View File

@ -33,7 +33,9 @@ export class CanonicalTileID implements ICanonicalTileID {
return this.z === id.z && this.x === id.x && this.y === id.y; return this.z === id.z && this.x === id.x && this.y === id.y;
} }
// given a list of urls, choose a url template and return a tile URL /**
* given a list of urls, choose a url template and return a tile URL
*/
url(urls: Array<string>, pixelRatio: number, scheme?: string | null) { url(urls: Array<string>, pixelRatio: number, scheme?: string | null) {
const bbox = getTileBBox(this.x, this.y, this.z); const bbox = getTileBBox(this.x, this.y, this.z);
const quadkey = getQuadkey(this.z, this.x, this.y); const quadkey = getQuadkey(this.z, this.x, this.y);
@ -113,6 +115,14 @@ export class OverscaledTileID {
return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical); return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical);
} }
/**
* Returns a new `OverscaledTileID` representing the tile at the target zoom level.
* When targetZ is greater than the current canonical z, the canonical coordinates are unchanged.
* When targetZ is less than the current canonical z, the canonical coordinates are updated.
* @param targetZ - the zoom level to scale to. Must be less than or equal to this.overscaledZ
* @returns a new OverscaledTileID representing the tile at the target zoom level
* @throws if targetZ is greater than this.overscaledZ
*/
scaledTo(targetZ: number) { scaledTo(targetZ: number) {
if (targetZ > this.overscaledZ) throw new Error(`targetZ > this.overscaledZ; targetZ = ${targetZ}; overscaledZ = ${this.overscaledZ}`); if (targetZ > this.overscaledZ) throw new Error(`targetZ > this.overscaledZ; targetZ = ${targetZ}; overscaledZ = ${this.overscaledZ}`);
const zDifference = this.canonical.z - targetZ; const zDifference = this.canonical.z - targetZ;
@ -123,6 +133,10 @@ export class OverscaledTileID {
} }
} }
isOverscaled() {
return (this.overscaledZ > this.canonical.z);
}
/* /*
* calculateScaledKey is an optimization: * calculateScaledKey is an optimization:
* when withWrap == true, implements the same as this.scaledTo(z).key, * when withWrap == true, implements the same as this.scaledTo(z).key,

View File

@ -14,6 +14,7 @@ import type {RemoveSourceParams} from '../util/actor_messages';
import type {IActor} from '../util/actor'; import type {IActor} from '../util/actor';
import type {StyleLayerIndex} from '../style/style_layer_index'; import type {StyleLayerIndex} from '../style/style_layer_index';
import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
import type {DashEntry} from '../render/line_atlas';
/** /**
* Parameters to identify a tile * Parameters to identify a tile
@ -59,6 +60,7 @@ export type WorkerDEMTileParameters = TileParameters & {
export type WorkerTileResult = ExpiryData & { export type WorkerTileResult = ExpiryData & {
buckets: Array<Bucket>; buckets: Array<Bucket>;
imageAtlas: ImageAtlas; imageAtlas: ImageAtlas;
dashPositions: Record<string, DashEntry>;
glyphAtlasImage: AlphaImage; glyphAtlasImage: AlphaImage;
featureIndex: FeatureIndex; featureIndex: FeatureIndex;
collisionBoxArray: CollisionBoxArray; collisionBoxArray: CollisionBoxArray;

View File

@ -11,6 +11,7 @@ import {type PossiblyEvaluated} from '../style/properties';
import {Color} from '@maplibre/maplibre-gl-style-spec'; import {Color} from '@maplibre/maplibre-gl-style-spec';
import {type CirclePaintProps, type CirclePaintPropsPossiblyEvaluated} from '../style/style_layer/circle_style_layer_properties.g'; import {type CirclePaintProps, type CirclePaintPropsPossiblyEvaluated} from '../style/style_layer/circle_style_layer_properties.g';
import {type SymbolLayoutProps, type SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties.g'; import {type SymbolLayoutProps, type SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties.g';
import {MessageType} from '../util/actor_messages';
function createWorkerTile(params?: {globalState?: Record<string, any>}): WorkerTile { function createWorkerTile(params?: {globalState?: Record<string, any>}): WorkerTile {
return new WorkerTile({ return new WorkerTile({
@ -175,6 +176,14 @@ describe('worker tile', () => {
'text-font': ['StandardFont-Bold'], 'text-font': ['StandardFont-Bold'],
'text-field': '{name}' 'text-field': '{name}'
} }
}, {
id: 'line-layer',
type: 'line',
source: 'source',
'source-layer': 'test',
paint: {
'line-dasharray': ['case', ['has', 'road_type'], ['literal', [2, 1]], ['literal', [1, 2]]]
}
}]); }]);
const data = { const data = {
@ -200,10 +209,16 @@ describe('worker tile', () => {
} as any as VectorTile; } as any as VectorTile;
const sendAsync = vi.fn().mockImplementation((message: {type: string; data: any}) => { const sendAsync = vi.fn().mockImplementation((message: {type: string; data: any}) => {
const response = message.type === 'getImages' ? if (message.type === MessageType.getImages) {
{'hello': {width: 1, height: 1, data: new Uint8Array([0])}} : return Promise.resolve({'hello': {width: 1, height: 1, data: new Uint8Array([0])}});
{'StandardFont-Bold': {width: 1, height: 1, data: new Uint8Array([0])}}; } else if (message.type === MessageType.getGlyphs) {
return Promise.resolve(response); return Promise.resolve({'StandardFont-Bold': {width: 1, height: 1, data: new Uint8Array([0])}});
} else if (message.type === MessageType.getDashes) {
return Promise.resolve({
'2,1,false': {y: 0, height: 16, width: 256},
'1,2,false': {y: 16, height: 16, width: 256}
});
}
}); });
const actorMock = { const actorMock = {
@ -211,10 +226,11 @@ describe('worker tile', () => {
}; };
const result = await tile.parse(data, layerIndex, ['hello'], actorMock, SubdivisionGranularitySetting.noSubdivision); const result = await tile.parse(data, layerIndex, ['hello'], actorMock, SubdivisionGranularitySetting.noSubdivision);
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(sendAsync).toHaveBeenCalledTimes(3); expect(sendAsync).toHaveBeenCalledTimes(4); // icons, patterns, glyphs, dashes
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({data: expect.objectContaining({'icons': ['hello'], 'type': 'icons'})}), expect.any(Object)); expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({type: 'GI', data: expect.objectContaining({'icons': ['hello'], 'type': 'icons'})}), expect.any(Object));
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({data: expect.objectContaining({'icons': ['hello'], 'type': 'patterns'})}), expect.any(Object)); expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({type: 'GI', data: expect.objectContaining({'icons': ['hello'], 'type': 'patterns'})}), expect.any(Object));
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({data: expect.objectContaining({'source': 'source', 'type': 'glyphs', 'stacks': {'StandardFont-Bold': [101, 115, 116]}})}), expect.any(Object)); expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({type: 'GG', data: expect.objectContaining({'source': 'source', 'type': 'glyphs', 'stacks': {'StandardFont-Bold': [101, 115, 116]}})}), expect.any(Object));
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({type: 'GDA', data: expect.objectContaining({'dashes': expect.any(Object)})}), expect.any(Object));
}); });
test('WorkerTile.parse would cancel and only event once on repeated reparsing', async () => { test('WorkerTile.parse would cancel and only event once on repeated reparsing', async () => {

View File

@ -22,7 +22,7 @@ import type {
} from '../source/worker_source'; } from '../source/worker_source';
import type {PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {VectorTile} from '@mapbox/vector-tile'; import type {VectorTile} from '@mapbox/vector-tile';
import {MessageType, type GetGlyphsResponse, type GetImagesResponse} from '../util/actor_messages'; import {type GetDashesResponse, MessageType, type GetGlyphsResponse, type GetImagesResponse} from '../util/actor_messages';
import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
export class WorkerTile { export class WorkerTile {
tileID: OverscaledTileID; tileID: OverscaledTileID;
@ -77,6 +77,7 @@ export class WorkerTile {
iconDependencies: {}, iconDependencies: {},
patternDependencies: {}, patternDependencies: {},
glyphDependencies: {}, glyphDependencies: {},
dashDependencies: {},
availableImages, availableImages,
subdivisionGranularity subdivisionGranularity
}; };
@ -107,10 +108,7 @@ export class WorkerTile {
if (layer.source !== this.source) { if (layer.source !== this.source) {
warnOnce(`layer.source = ${layer.source} does not equal this.source = ${this.source}`); warnOnce(`layer.source = ${layer.source} does not equal this.source = ${this.source}`);
} }
if (layer.minzoom && this.zoom < Math.floor(layer.minzoom)) continue; if (layer.isHidden(this.zoom, true)) continue;
if (layer.maxzoom && this.zoom >= layer.maxzoom) continue;
if (layer.visibility === 'none') continue;
recalculateLayers(family, this.zoom, availableImages); recalculateLayers(family, this.zoom, availableImages);
const bucket = buckets[layer.id] = layer.createBucket({ const bucket = buckets[layer.id] = layer.createBucket({
@ -159,7 +157,16 @@ export class WorkerTile {
getPatternsPromise = actor.sendAsync({type: MessageType.getImages, data: {icons: patterns, source: this.source, tileID: this.tileID, type: 'patterns'}}, abortController); getPatternsPromise = actor.sendAsync({type: MessageType.getImages, data: {icons: patterns, source: this.source, tileID: this.tileID, type: 'patterns'}}, abortController);
} }
const [glyphMap, iconMap, patternMap] = await Promise.all([getGlyphsPromise, getIconsPromise, getPatternsPromise]); const dashes = options.dashDependencies;
let getDashesPromise = Promise.resolve<GetDashesResponse>({} as GetDashesResponse);
if (Object.keys(dashes).length) {
const abortController = new AbortController();
this.inFlightDependencies.push(abortController);
getDashesPromise = actor.sendAsync({type: MessageType.getDashes, data: {dashes}}, abortController);
}
const [glyphMap, iconMap, patternMap, dashPositions] = await Promise.all([getGlyphsPromise, getIconsPromise, getPatternsPromise, getDashesPromise]);
const glyphAtlas = new GlyphAtlas(glyphMap); const glyphAtlas = new GlyphAtlas(glyphMap);
const imageAtlas = new ImageAtlas(iconMap, patternMap); const imageAtlas = new ImageAtlas(iconMap, patternMap);
@ -177,12 +184,9 @@ export class WorkerTile {
canonical: this.tileID.canonical, canonical: this.tileID.canonical,
subdivisionGranularity: options.subdivisionGranularity subdivisionGranularity: options.subdivisionGranularity
}); });
} else if (bucket.hasPattern && } else if (bucket.hasDependencies && (bucket instanceof FillBucket || bucket instanceof FillExtrusionBucket || bucket instanceof LineBucket)) {
(bucket instanceof LineBucket ||
bucket instanceof FillBucket ||
bucket instanceof FillExtrusionBucket)) {
recalculateLayers(bucket.layers, this.zoom, availableImages); recalculateLayers(bucket.layers, this.zoom, availableImages);
bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions); bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions, dashPositions);
} }
} }
@ -193,6 +197,7 @@ export class WorkerTile {
collisionBoxArray: this.collisionBoxArray, collisionBoxArray: this.collisionBoxArray,
glyphAtlasImage: glyphAtlas.image, glyphAtlasImage: glyphAtlas.image,
imageAtlas, imageAtlas,
dashPositions,
// Only used for benchmarking: // Only used for benchmarking:
glyphMap: this.returnDependencies ? glyphMap : null, glyphMap: this.returnDependencies ? glyphMap : null,
iconMap: this.returnDependencies ? iconMap : null, iconMap: this.returnDependencies ? iconMap : null,

View File

@ -1,4 +1,4 @@
import {browser} from '../util/browser'; import {now} from '../util/time_control';
import {Placement} from '../symbol/placement'; import {Placement} from '../symbol/placement';
import type {ITransform} from '../geo/transform_interface'; import type {ITransform} from '../geo/transform_interface';
import type {StyleLayer} from './style_layer'; import type {StyleLayer} from './style_layer';
@ -92,10 +92,10 @@ export class PauseablePlacement {
layers: {[_: string]: StyleLayer}, layers: {[_: string]: StyleLayer},
layerTiles: {[_: string]: Array<Tile>} layerTiles: {[_: string]: Array<Tile>}
) { ) {
const startTime = browser.now(); const startTime = now();
const shouldPausePlacement = () => { const shouldPausePlacement = () => {
return this._forceFullPlacement ? false : (browser.now() - startTime) > 2; return this._forceFullPlacement ? false : (now() - startTime) > 2;
}; };
while (this._currentPlacementIndex >= 0) { while (this._currentPlacementIndex >= 0) {

View File

@ -3378,4 +3378,33 @@ describe('Style.serialize', () => {
expect(style.sky.properties.get('fog-color').g).toBe(1); expect(style.sky.properties.get('fog-color').g).toBe(1);
expect(style.sky.properties.get('fog-color').r).toBe(0); expect(style.sky.properties.get('fog-color').r).toBe(0);
}); });
test('Style.getDashes returns line atlas entries for dash patterns', async () => {
const style = createStyle();
const params = {
dashes: {
'2,1,false': {dasharray: [2, 1], round: false},
'4,2,true': {dasharray: [4, 2], round: true}
},
source: 'test-source',
tileID: {z: 1, x: 0, y: 0} as any,
type: 'dasharray' as const
};
const result = await style.getDashes('mapId', params);
expect(result).toBeDefined();
expect(Object.keys(result)).toHaveLength(2);
expect(result['2,1,false']).toBeDefined();
expect(result['4,2,true']).toBeDefined();
// Verify the entries have the expected atlas properties
expect(typeof result['2,1,false'].width).toBe('number');
expect(typeof result['2,1,false'].height).toBe('number');
expect(typeof result['2,1,false'].y).toBe('number');
expect(typeof result['4,2,true'].width).toBe('number');
expect(typeof result['4,2,true'].height).toBe('number');
expect(typeof result['4,2,true'].y).toBe('number');
});
}); });

View File

@ -1,5 +1,6 @@
import {Event, ErrorEvent, Evented} from '../util/evented'; import {Event, ErrorEvent, Evented} from '../util/evented';
import {type StyleLayer} from './style_layer'; import {type StyleLayer} from './style_layer';
import {isRasterStyleLayer} from './style_layer/raster_style_layer';
import {createStyleLayer} from './create_style_layer'; import {createStyleLayer} from './create_style_layer';
import {loadSprite} from './load_sprite'; import {loadSprite} from './load_sprite';
import {ImageManager} from '../render/image_manager'; import {ImageManager} from '../render/image_manager';
@ -12,6 +13,7 @@ import {coerceSpriteToArray} from '../util/style';
import {getJSON, getReferrer} from '../util/ajax'; import {getJSON, getReferrer} from '../util/ajax';
import {ResourceType} from '../util/request_manager'; import {ResourceType} from '../util/request_manager';
import {browser} from '../util/browser'; import {browser} from '../util/browser';
import {now} from '../util/time_control';
import {Dispatcher} from '../util/dispatcher'; import {Dispatcher} from '../util/dispatcher';
import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style'; import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style';
import {type Source} from '../source/source'; import {type Source} from '../source/source';
@ -59,6 +61,8 @@ import type {CanvasSourceSpecification} from '../source/canvas_source';
import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {CustomLayerInterface} from './style_layer/custom_style_layer';
import type {Validator} from './validate_style'; import type {Validator} from './validate_style';
import { import {
type GetDashesParameters,
type GetDashesResponse,
MessageType, MessageType,
type GetGlyphsParameters, type GetGlyphsParameters,
type GetGlyphsResponse, type GetGlyphsResponse,
@ -247,6 +251,9 @@ export class Style extends Evented {
this.dispatcher.registerMessageHandler(MessageType.getImages, (mapId, params) => { this.dispatcher.registerMessageHandler(MessageType.getImages, (mapId, params) => {
return this.getImages(mapId, params); return this.getImages(mapId, params);
}); });
this.dispatcher.registerMessageHandler(MessageType.getDashes, (mapId, params) => {
return this.getDashes(mapId, params);
});
this.imageManager = new ImageManager(); this.imageManager = new ImageManager();
this.imageManager.setEventedParent(this); this.imageManager.setEventedParent(this);
const glyphLang = map._container?.lang || (typeof document !== 'undefined' && document.documentElement?.lang) || undefined; const glyphLang = map._container?.lang || (typeof document !== 'undefined' && document.documentElement?.lang) || undefined;
@ -475,6 +482,11 @@ export class Style extends Evented {
const styledLayer = createStyleLayer(layer, this._globalState); const styledLayer = createStyleLayer(layer, this._globalState);
styledLayer.setEventedParent(this, {layer: {id: layer.id}}); styledLayer.setEventedParent(this, {layer: {id: layer.id}});
this._layers[layer.id] = styledLayer; this._layers[layer.id] = styledLayer;
if (isRasterStyleLayer(styledLayer) && this.sourceCaches[styledLayer.source]) {
const rasterFadeDuration = layer.paint?.['raster-fade-duration'] ?? styledLayer.paint.get('raster-fade-duration');
this.sourceCaches[styledLayer.source].setRasterFadeDuration(rasterFadeDuration);
}
} }
} }
@ -1314,6 +1326,10 @@ export class Style extends Evented {
this._updateLayer(layer); this._updateLayer(layer);
} }
if (isRasterStyleLayer(layer) && name === 'raster-fade-duration') {
this.sourceCaches[layer.source].setRasterFadeDuration(value);
}
this._changed = true; this._changed = true;
this._updatedPaintProps[layer.id] = true; this._updatedPaintProps[layer.id] = true;
// reset serialization field, to be populated only when needed // reset serialization field, to be populated only when needed
@ -1619,7 +1635,7 @@ export class Style extends Evented {
if (!_update) return; if (!_update) return;
const parameters = { const parameters = {
now: browser.now(), now: now(),
transition: extend({ transition: extend({
duration: 300, duration: 300,
delay: 0 delay: 0
@ -1671,7 +1687,7 @@ export class Style extends Evented {
if (!update) return; if (!update) return;
const parameters = { const parameters = {
now: browser.now(), now: now(),
transition: extend({ transition: extend({
duration: 300, duration: 300,
delay: 0 delay: 0
@ -1684,7 +1700,7 @@ export class Style extends Evented {
} }
_setProjectionInternal(name: ProjectionSpecification['type']) { _setProjectionInternal(name: ProjectionSpecification['type']) {
const projectionObjects = createProjectionFromName(name); const projectionObjects = createProjectionFromName(name, this.map.transformConstrain);
this.projection = projectionObjects.projection; this.projection = projectionObjects.projection;
this.map.migrateProjection(projectionObjects.transform, projectionObjects.cameraHelper); this.map.migrateProjection(projectionObjects.transform, projectionObjects.cameraHelper);
for (const key in this.sourceCaches) { for (const key in this.sourceCaches) {
@ -1788,7 +1804,7 @@ export class Style extends Evented {
// tiles will fully display symbols in their first frame // tiles will fully display symbols in their first frame
forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0; forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0;
if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) { if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(now(), transform.zoom))) {
this.pauseablePlacement = new PauseablePlacement(transform, this.map.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement); this.pauseablePlacement = new PauseablePlacement(transform, this.map.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
this._layerOrderChanged = false; this._layerOrderChanged = false;
} }
@ -1803,7 +1819,7 @@ export class Style extends Evented {
this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles);
if (this.pauseablePlacement.isDone()) { if (this.pauseablePlacement.isDone()) {
this.placement = this.pauseablePlacement.commit(browser.now()); this.placement = this.pauseablePlacement.commit(now());
placementCommitted = true; placementCommitted = true;
} }
@ -1824,7 +1840,7 @@ export class Style extends Evented {
} }
// needsRender is false when we have just finished a placement that didn't change the visibility of any symbols // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols
const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now()); const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(now());
return needsRerender; return needsRerender;
} }
@ -1883,6 +1899,14 @@ export class Style extends Evented {
this.glyphManager.setURL(glyphsUrl); this.glyphManager.setURL(glyphsUrl);
} }
async getDashes(mapId: string | number, params: GetDashesParameters): Promise<GetDashesResponse> {
const result: GetDashesResponse = {};
for (const [key, dash] of Object.entries(params.dashes)) {
result[key] = this.lineAtlas.getDash(dash.dasharray, dash.round);
}
return result;
}
/** /**
* Add a sprite. * Add a sprite.
* *

View File

@ -283,8 +283,8 @@ export abstract class StyleLayer extends Evented {
return false; return false;
} }
isHidden(zoom: number) { isHidden(zoom: number, roundMinZoom: boolean = false) {
if (this.minzoom && zoom < this.minzoom) return true; if (this.minzoom && zoom < (roundMinZoom ? Math.floor(this.minzoom) : this.minzoom)) return true;
if (this.maxzoom && zoom >= this.maxzoom) return true; if (this.maxzoom && zoom >= this.maxzoom) return true;
return this.visibility === 'none'; return this.visibility === 'none';
} }

View File

@ -47,7 +47,7 @@ describe('CircleStyleLayer.queryIntersectsFeature', () => {
} }
describe('Mercator projection', () => { describe('Mercator projection', () => {
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(400, 300); transform.resize(400, 300);
describe('map pitch alignment', () => { describe('map pitch alignment', () => {

View File

@ -39,7 +39,7 @@ describe('HeatmapStyleLayer.queryIntersectsFeature', () => {
} }
describe('Mercator projection', () => { describe('Mercator projection', () => {
const transform = new MercatorTransform(0, 22, 0, 85, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(400, 300); transform.resize(400, 300);
test('returns `true` when a heatmap intersects a point', () => { test('returns `true` when a heatmap intersects a point', () => {

View File

@ -51,7 +51,7 @@ export type LinePaintProps = {
"line-gap-width": DataDrivenProperty<number>, "line-gap-width": DataDrivenProperty<number>,
"line-offset": DataDrivenProperty<number>, "line-offset": DataDrivenProperty<number>,
"line-blur": DataDrivenProperty<number>, "line-blur": DataDrivenProperty<number>,
"line-dasharray": CrossFadedProperty<Array<number>>, "line-dasharray": CrossFadedDataDrivenProperty<Array<number>>,
"line-pattern": CrossFadedDataDrivenProperty<ResolvedImage>, "line-pattern": CrossFadedDataDrivenProperty<ResolvedImage>,
"line-gradient": ColorRampProperty, "line-gradient": ColorRampProperty,
}; };
@ -65,7 +65,7 @@ export type LinePaintPropsPossiblyEvaluated = {
"line-gap-width": PossiblyEvaluatedPropertyValue<number>, "line-gap-width": PossiblyEvaluatedPropertyValue<number>,
"line-offset": PossiblyEvaluatedPropertyValue<number>, "line-offset": PossiblyEvaluatedPropertyValue<number>,
"line-blur": PossiblyEvaluatedPropertyValue<number>, "line-blur": PossiblyEvaluatedPropertyValue<number>,
"line-dasharray": CrossFaded<Array<number>>, "line-dasharray": PossiblyEvaluatedPropertyValue<CrossFaded<Array<number>>>,
"line-pattern": PossiblyEvaluatedPropertyValue<CrossFaded<ResolvedImage>>, "line-pattern": PossiblyEvaluatedPropertyValue<CrossFaded<ResolvedImage>>,
"line-gradient": ColorRampProperty, "line-gradient": ColorRampProperty,
}; };
@ -80,7 +80,7 @@ const getPaint = () => paint = paint || new Properties({
"line-gap-width": new DataDrivenProperty(styleSpec["paint_line"]["line-gap-width"] as any as StylePropertySpecification), "line-gap-width": new DataDrivenProperty(styleSpec["paint_line"]["line-gap-width"] as any as StylePropertySpecification),
"line-offset": new DataDrivenProperty(styleSpec["paint_line"]["line-offset"] as any as StylePropertySpecification), "line-offset": new DataDrivenProperty(styleSpec["paint_line"]["line-offset"] as any as StylePropertySpecification),
"line-blur": new DataDrivenProperty(styleSpec["paint_line"]["line-blur"] as any as StylePropertySpecification), "line-blur": new DataDrivenProperty(styleSpec["paint_line"]["line-blur"] as any as StylePropertySpecification),
"line-dasharray": new CrossFadedProperty(styleSpec["paint_line"]["line-dasharray"] as any as StylePropertySpecification), "line-dasharray": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-dasharray"] as any as StylePropertySpecification),
"line-pattern": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-pattern"] as any as StylePropertySpecification), "line-pattern": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-pattern"] as any as StylePropertySpecification),
"line-gradient": new ColorRampProperty(styleSpec["paint_line"]["line-gradient"] as any as StylePropertySpecification), "line-gradient": new ColorRampProperty(styleSpec["paint_line"]["line-gradient"] as any as StylePropertySpecification),
}); });

View File

@ -7,7 +7,7 @@ import {mat4} from 'gl-matrix';
describe('CollisionIndex', () => { describe('CollisionIndex', () => {
test('floating point precision', () => { test('floating point precision', () => {
const x = 100000.123456, y = 0; const x = 100000.123456, y = 0;
const transform = new MercatorTransform(0, 22, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(200, 200); transform.resize(200, 200);
const tile = new UnwrappedTileID(0, new CanonicalTileID(0, 0, 0)); const tile = new UnwrappedTileID(0, new CanonicalTileID(0, 0, 0));
vi.spyOn(transform, 'calculatePosMatrix').mockImplementation(() => mat4.create()); vi.spyOn(transform, 'calculatePosMatrix').mockImplementation(() => mat4.create());

View File

@ -11,6 +11,7 @@ import type {IReadonlyTransform} from '../geo/transform_interface';
import type {SingleCollisionBox} from '../data/bucket/symbol_bucket'; import type {SingleCollisionBox} from '../data/bucket/symbol_bucket';
import type { import type {
GlyphOffsetArray, GlyphOffsetArray,
PlacedSymbol,
SymbolLineVertexArray SymbolLineVertexArray
} from '../data/array_types.g'; } from '../data/array_types.g';
import type {OverlapMode} from '../style/style_layer/overlap_mode'; import type {OverlapMode} from '../style/style_layer/overlap_mode';
@ -184,7 +185,7 @@ export class CollisionIndex {
placeCollisionCircles( placeCollisionCircles(
overlapMode: OverlapMode, overlapMode: OverlapMode,
symbol: any, symbol: PlacedSymbol,
lineVertexArray: SymbolLineVertexArray, lineVertexArray: SymbolLineVertexArray,
glyphOffsetArray: GlyphOffsetArray, glyphOffsetArray: GlyphOffsetArray,
fontSize: number, fontSize: number,

View File

@ -78,7 +78,7 @@ function getAnchors(line: Array<Point>,
glyphSize: number, glyphSize: number,
boxScale: number, boxScale: number,
overscaling: number, overscaling: number,
tileExtent: number) { tileExtent: number): Anchor[] {
// Resample a line to get anchor points for labels and check that each // Resample a line to get anchor points for labels and check that each
// potential label passes text-max-angle check and has enough room to fit // potential label passes text-max-angle check and has enough room to fit
@ -111,15 +111,15 @@ function getAnchors(line: Array<Point>,
return resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, false, tileExtent); return resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, false, tileExtent);
} }
function resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, placeAtMiddle, tileExtent) { function resample(line: Point[], offset: number, spacing: number, angleWindowSize: number, maxAngle: number, labelLength: number, isLineContinued: boolean, placeAtMiddle: boolean, tileExtent: number): Anchor[] {
const halfLabelLength = labelLength / 2; const halfLabelLength = labelLength / 2;
const lineLength = getLineLength(line); const lineLength = getLineLength(line);
let distance = 0, let distance = 0;
markedDistance = offset - spacing; let markedDistance = offset - spacing;
let anchors = []; let anchors: Anchor[] = [];
for (let i = 0; i < line.length - 1; i++) { for (let i = 0; i < line.length - 1; i++) {

View File

@ -294,7 +294,7 @@ export class Placement {
pitchedLabelPlaneMatrix, pitchedLabelPlaneMatrix,
scale, scale,
textPixelRatio, textPixelRatio,
holdingForFade: tile.holdingForFade(), holdingForFade: tile.holdingForSymbolFade(),
collisionBoxArray, collisionBoxArray,
partiallyEvaluatedTextSize: symbolSize.evaluateSizeForZoom(symbolBucket.textSizeData, this.transform.zoom), partiallyEvaluatedTextSize: symbolSize.evaluateSizeForZoom(symbolBucket.textSizeData, this.transform.zoom),
collisionGroup: this.collisionGroups.get(symbolBucket.sourceID) collisionGroup: this.collisionGroups.get(symbolBucket.sourceID)
@ -683,7 +683,7 @@ export class Placement {
placeText = placedGlyphBoxes && placedGlyphBoxes.placeable; placeText = placedGlyphBoxes && placedGlyphBoxes.placeable;
offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen; offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen;
if (symbolInstance.useRuntimeCollisionCircles) { if (symbolInstance.useRuntimeCollisionCircles && symbolInstance.centerJustifiedTextSymbolIndex >= 0) {
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex); const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol); const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol);

View File

@ -10,7 +10,8 @@ import type {SymbolBucket} from '../data/bucket/symbol_bucket';
import type { import type {
GlyphOffsetArray, GlyphOffsetArray,
SymbolLineVertexArray, SymbolLineVertexArray,
SymbolDynamicLayoutArray SymbolDynamicLayoutArray,
PlacedSymbol,
} from '../data/array_types.g'; } from '../data/array_types.g';
import {WritingMode} from '../symbol/shaping'; import {WritingMode} from '../symbol/shaping';
import {findLineIntersection} from '../util/util'; import {findLineIntersection} from '../util/util';
@ -344,7 +345,7 @@ export function placeFirstAndLastGlyph(
lineOffsetX: number, lineOffsetX: number,
lineOffsetY: number, lineOffsetY: number,
flip: boolean, flip: boolean,
symbol: any, symbol: PlacedSymbol,
rotateToLine: boolean, rotateToLine: boolean,
projectionContext: SymbolProjectionContext): FirstAndLastGlyphPlacement { projectionContext: SymbolProjectionContext): FirstAndLastGlyphPlacement {
const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs;
@ -404,7 +405,7 @@ type GlyphLinePlacementResult = OrientationChangeType & {
type GlyphLinePlacementArgs = { type GlyphLinePlacementArgs = {
projectionContext: SymbolProjectionContext; projectionContext: SymbolProjectionContext;
pitchedLabelPlaneMatrixInverse: mat4; pitchedLabelPlaneMatrixInverse: mat4;
symbol: any; // PlacedSymbolStruct symbol: PlacedSymbol;
fontSize: number; fontSize: number;
flip: boolean; flip: boolean;
keepUpright: boolean; keepUpright: boolean;

View File

@ -321,7 +321,7 @@ function addFeature(bucket: SymbolBucket,
textRepeatDistance = symbolMinDistance / 2; textRepeatDistance = symbolMinDistance / 2;
const iconTextFit = layout.get('icon-text-fit'); const iconTextFit = layout.get('icon-text-fit');
let verticallyShapedIcon; let verticallyShapedIcon: PositionedIcon | undefined;
// Adjust shaped icon size when icon-text-fit is used. // Adjust shaped icon size when icon-text-fit is used.
if (shapedIcon && iconTextFit !== 'none') { if (shapedIcon && iconTextFit !== 'none') {
if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) { if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) {
@ -509,9 +509,9 @@ function addSymbol(bucket: SymbolBucket,
anchor: Anchor, anchor: Anchor,
line: Array<Point>, line: Array<Point>,
shapedTextOrientations: ShapedTextOrientations, shapedTextOrientations: ShapedTextOrientations,
shapedIcon: PositionedIcon | void, shapedIcon: PositionedIcon | undefined,
imageMap: {[_: string]: StyleImage}, imageMap: {[_: string]: StyleImage},
verticallyShapedIcon: PositionedIcon | void, verticallyShapedIcon: PositionedIcon | undefined,
layer: SymbolStyleLayer, layer: SymbolStyleLayer,
collisionBoxArray: CollisionBoxArray, collisionBoxArray: CollisionBoxArray,
featureIndex: number, featureIndex: number,

View File

@ -1,6 +1,7 @@
import {describe, beforeEach, test, expect, vi} from 'vitest'; import {describe, beforeEach, test, expect, vi} from 'vitest';
import {Camera, type CameraOptions, type PointLike} from '../ui/camera'; import {Camera, type CameraOptions, type PointLike} from '../ui/camera';
import {TaskQueue, type TaskID} from '../util/task_queue'; import {TaskQueue, type TaskID} from '../util/task_queue';
import * as timeControl from '../util/time_control';
import {browser} from '../util/browser'; import {browser} from '../util/browser';
import {fixedLngLat, fixedNum} from '../../test/unit/lib/fixed'; import {fixedLngLat, fixedNum} from '../../test/unit/lib/fixed';
import {setMatchMedia} from '../util/test/util'; import {setMatchMedia} from '../util/test/util';
@ -1042,7 +1043,7 @@ describe('easeTo', () => {
test('can be called from within a moveend event handler', async () => { test('can be called from within a moveend event handler', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.easeTo({center: [100, 0], duration: 10}); camera.easeTo({center: [100, 0], duration: 10});
@ -1079,7 +1080,7 @@ describe('easeTo', () => {
test('pans eastward across the antimeridian', async () => { test('pans eastward across the antimeridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1122,7 +1123,7 @@ describe('easeTo', () => {
test('pans westward across the antimeridian', async () => { test('pans westward across the antimeridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1166,7 +1167,7 @@ describe('easeTo', () => {
test('animation occurs when prefers-reduced-motion: reduce is set but overridden by essential: true', async () => { test('animation occurs when prefers-reduced-motion: reduce is set but overridden by essential: true', async () => {
const camera = createCamera(); const camera = createCamera();
Object.defineProperty(browser, 'prefersReducedMotion', {value: true}); Object.defineProperty(browser, 'prefersReducedMotion', {value: true});
const stubNow = vi.spyOn(browser, 'now'); const stubNow = vi.spyOn(timeControl, 'now');
// camera transition expected to take in this range when prefersReducedMotion is set and essential: true, // camera transition expected to take in this range when prefersReducedMotion is set and essential: true,
// when a duration of 200 is requested // when a duration of 200 is requested
@ -1174,7 +1175,7 @@ describe('easeTo', () => {
const max = 300; const max = 300;
let startTime; let startTime;
camera.on('movestart', () => { startTime = browser.now(); }); camera.on('movestart', () => { startTime = timeControl.now(); });
const promise = camera.once('moveend'); const promise = camera.once('moveend');
setTimeout(() => { setTimeout(() => {
@ -1190,7 +1191,7 @@ describe('easeTo', () => {
}, 0); }, 0);
await promise; await promise;
const endTime = browser.now(); const endTime = timeControl.now();
const timeDiff = endTime - startTime; const timeDiff = endTime - startTime;
expect(timeDiff >= min && timeDiff < max).toBeTruthy(); expect(timeDiff >= min && timeDiff < max).toBeTruthy();
}); });
@ -1281,12 +1282,12 @@ describe('easeTo', () => {
test('terrain set during easeTo', () => { test('terrain set during easeTo', () => {
const camera = createCamera(); const camera = createCamera();
const stubNow = vi.spyOn(browser, 'now'); const stubNow = vi.spyOn(timeControl, 'now');
stubNow.mockImplementation(() => 0); stubNow.mockImplementation(() => 0);
camera.easeTo({bearing: 97, duration: 500}); camera.easeTo({bearing: 97, duration: 500});
stubNow.mockImplementation(() => 100); stubNow.mockImplementation(() => 100);
camera.simulateFrame(); camera.simulateFrame();
@ -1321,7 +1322,7 @@ describe('flyTo', () => {
}); });
test('does not throw when cameras current zoom is above maxzoom and an offset creates infinite zoom out factor', () => { test('does not throw when cameras current zoom is above maxzoom and an offset creates infinite zoom out factor', () => {
const transform = new MercatorTransform(0, 20.9999, 0, 60, true); const transform = new MercatorTransform({minZoom: 0, maxZoom: 20.9999, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(512, 512); transform.resize(512, 512);
const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any)) const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any))
.jumpTo({zoom: 21, center: [0, 0]}); .jumpTo({zoom: 21, center: [0, 0]});
@ -1344,7 +1345,7 @@ describe('flyTo', () => {
test('Zoom out from the same position to the same position with animation', async () => { test('Zoom out from the same position to the same position with animation', async () => {
const pos = {lng: 0, lat: 0}; const pos = {lng: 0, lat: 0};
const camera = createCamera({zoom: 20, center: pos}); const camera = createCamera({zoom: 20, center: pos});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const promise = camera.once('moveend'); const promise = camera.once('moveend');
@ -1538,7 +1539,7 @@ describe('flyTo', () => {
camera.on('pitchend', (d) => { pitchended = d.data; }); camera.on('pitchend', (d) => { pitchended = d.data; });
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [100, 0], duration: 10}, eventData); camera.flyTo({center: [100, 0], duration: 10}, eventData);
@ -1579,7 +1580,7 @@ describe('flyTo', () => {
}); });
test('no roll when motion is interrupted', () => { test('no roll when motion is interrupted', () => {
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const camera = createCamera(); const camera = createCamera();
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -1591,7 +1592,7 @@ describe('flyTo', () => {
}); });
test('no roll when motion is interrupted: globe', () => { test('no roll when motion is interrupted: globe', () => {
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const camera = createCameraGlobe(); const camera = createCameraGlobe();
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -1603,7 +1604,7 @@ describe('flyTo', () => {
}); });
test('angles when motion is interrupted', () => { test('angles when motion is interrupted', () => {
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const camera = createCamera(); const camera = createCamera();
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -1617,7 +1618,7 @@ describe('flyTo', () => {
}); });
test('angles when motion is interrupted: globe', () => { test('angles when motion is interrupted: globe', () => {
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const camera = createCameraGlobe(); const camera = createCameraGlobe();
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -1632,7 +1633,7 @@ describe('flyTo', () => {
test('can be called from within a moveend event handler', async () => { test('can be called from within a moveend event handler', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [100, 0], duration: 10}); camera.flyTo({center: [100, 0], duration: 10});
@ -1673,7 +1674,7 @@ describe('flyTo', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [100, 0], zoom: 18, duration: 10}); camera.flyTo({center: [100, 0], zoom: 18, duration: 10});
@ -1693,7 +1694,7 @@ describe('flyTo', () => {
test('pans eastward across the prime meridian', async () => { test('pans eastward across the prime meridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-10, 0]); camera.setCenter([-10, 0]);
let crossedPrimeMeridian; let crossedPrimeMeridian;
@ -1725,7 +1726,7 @@ describe('flyTo', () => {
test('pans westward across the prime meridian', async () => { test('pans westward across the prime meridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([10, 0]); camera.setCenter([10, 0]);
let crossedPrimeMeridian; let crossedPrimeMeridian;
@ -1757,7 +1758,7 @@ describe('flyTo', () => {
test('pans eastward across the antimeridian', async () => { test('pans eastward across the antimeridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1788,7 +1789,7 @@ describe('flyTo', () => {
test('pans westward across the antimeridian', async () => { test('pans westward across the antimeridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1819,7 +1820,7 @@ describe('flyTo', () => {
test('does not pan eastward across the antimeridian if no world copies', async () => { test('does not pan eastward across the antimeridian if no world copies', async () => {
const camera = createCamera({renderWorldCopies: false}); const camera = createCamera({renderWorldCopies: false});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1851,7 +1852,7 @@ describe('flyTo', () => {
test('does not pan westward across the antimeridian if no world copies', async () => { test('does not pan westward across the antimeridian if no world copies', async () => {
const camera = createCamera({renderWorldCopies: false}); const camera = createCamera({renderWorldCopies: false});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -1883,7 +1884,7 @@ describe('flyTo', () => {
test('jumps back to world 0 when crossing the antimeridian', async () => { test('jumps back to world 0 when crossing the antimeridian', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
@ -1914,7 +1915,7 @@ describe('flyTo', () => {
test('peaks at the specified zoom level', async () => { test('peaks at the specified zoom level', async () => {
const camera = createCamera({zoom: 20}); const camera = createCamera({zoom: 20});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const minZoom = 1; const minZoom = 1;
let zoomed = false; let zoomed = false;
@ -1950,7 +1951,7 @@ describe('flyTo', () => {
}); });
test('respects transform\'s maxZoom', async () => { test('respects transform\'s maxZoom', async () => {
const transform = new MercatorTransform(2, 10, 0, 60, false); const transform = new MercatorTransform({minZoom: 2, maxZoom: 10, minPitch: 0, maxPitch: 60, renderWorldCopies: false});
transform.resize(512, 512); transform.resize(512, 512);
const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any)); const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any));
@ -1958,7 +1959,7 @@ describe('flyTo', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [12, 34], zoom: 30, duration: 10}); camera.flyTo({center: [12, 34], zoom: 30, duration: 10});
@ -1975,7 +1976,7 @@ describe('flyTo', () => {
}); });
test('respects transform\'s minZoom', async () => { test('respects transform\'s minZoom', async () => {
const transform = new MercatorTransform(2, 10, 0, 60, false); const transform = new MercatorTransform({minZoom: 2, maxZoom: 10, minPitch: 0, maxPitch: 60, renderWorldCopies: false});
transform.resize(512, 512); transform.resize(512, 512);
const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any)); const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any));
@ -1983,7 +1984,7 @@ describe('flyTo', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [12, 34], zoom: 1, duration: 10}); camera.flyTo({center: [12, 34], zoom: 1, duration: 10});
@ -2029,9 +2030,18 @@ describe('flyTo', () => {
expect(timeDiff >= 0 && timeDiff < 10).toBeTruthy(); expect(timeDiff >= 0 && timeDiff < 10).toBeTruthy();
}); });
test('applies the padding option when prefers-reduce-motion:reduce is set', async () => {
const camera = createCamera();
Object.defineProperty(browser, 'prefersReducedMotion', {value: true});
camera.flyTo({padding: {top: 50, right: 30}});
expect(camera.getPadding()).toEqual({top: 50, bottom: 0, left: 0, right: 30});
});
test('check elevation events freezeElevation=false', async () => { test('check elevation events freezeElevation=false', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const terrainCallbacks = {prepare: 0, update: 0, finalize: 0} as any; const terrainCallbacks = {prepare: 0, update: 0, finalize: 0} as any;
camera.terrain = {} as Terrain; camera.terrain = {} as Terrain;
@ -2055,7 +2065,7 @@ describe('flyTo', () => {
test('check elevation events freezeElevation=true', async() => { test('check elevation events freezeElevation=true', async() => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const terrainCallbacks = {prepare: 0, update: 0, finalize: 0} as any; const terrainCallbacks = {prepare: 0, update: 0, finalize: 0} as any;
camera.terrain = {} as Terrain; camera.terrain = {} as Terrain;
@ -2121,7 +2131,7 @@ describe('isEasing', () => {
test('returns false when done panning', async () => { test('returns false when done panning', async () => {
const camera = createCamera(); const camera = createCamera();
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.panTo([100, 0], {duration: 1}); camera.panTo([100, 0], {duration: 1});
setTimeout(() => { setTimeout(() => {
@ -2143,7 +2153,7 @@ describe('isEasing', () => {
test('returns false when done zooming', async () => { test('returns false when done zooming', async () => {
const camera = createCamera(); const camera = createCamera();
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.zoomTo(3.2, {duration: 1}); camera.zoomTo(3.2, {duration: 1});
setTimeout(() => { setTimeout(() => {
@ -2164,7 +2174,7 @@ describe('isEasing', () => {
test('returns false when done rotating', async () => { test('returns false when done rotating', async () => {
const camera = createCamera(); const camera = createCamera();
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.rotateTo(90, {duration: 1}); camera.rotateTo(90, {duration: 1});
setTimeout(() => { setTimeout(() => {
@ -2239,7 +2249,7 @@ describe('stop', () => {
const spy = vi.fn(); const spy = vi.fn();
camera.on('moveend', spy); camera.on('moveend', spy);
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.panTo([100, 0], {duration: 1}, eventData); camera.panTo([100, 0], {duration: 1}, eventData);
@ -2344,7 +2354,7 @@ describe('cameraForBounds', () => {
}); });
test('asymmetrical transform using LngLatBounds instance', () => { test('asymmetrical transform using LngLatBounds instance', () => {
const transform = new MercatorTransform(2, 10, 0, 60, false); const transform = new MercatorTransform({minZoom: 2, maxZoom: 10, minPitch: 0, maxPitch: 60, renderWorldCopies: false});
transform.resize(2048, 512); transform.resize(2048, 512);
const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any)); const camera = attachSimulateFrame(new CameraMock(transform, new MercatorCameraHelper(), {} as any));
@ -2477,7 +2487,7 @@ describe('queryTerrainElevation', () => {
test('Calls getElevationForLngLatZoom with correct arguments', () => { test('Calls getElevationForLngLatZoom with correct arguments', () => {
const getElevationForLngLatZoom = vi.fn(); const getElevationForLngLatZoom = vi.fn();
camera.terrain = {getElevationForLngLatZoom} as any as Terrain; camera.terrain = {getElevationForLngLatZoom} as any as Terrain;
camera.transform = new MercatorTransform(0, 22, 0, 60, true); camera.transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
camera.queryTerrainElevation([1, 2]); camera.queryTerrainElevation([1, 2]);
@ -2514,7 +2524,7 @@ describe('transformCameraUpdate', () => {
test('invoke transformCameraUpdate callback during easeTo', async () => { test('invoke transformCameraUpdate callback during easeTo', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
let callbackCount = 0; let callbackCount = 0;
@ -2548,7 +2558,7 @@ describe('transformCameraUpdate', () => {
test('invoke transformCameraUpdate callback during flyTo', async () => { test('invoke transformCameraUpdate callback during flyTo', async () => {
const camera = createCamera(); const camera = createCamera();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
let callbackCount = 0; let callbackCount = 0;
@ -2858,7 +2868,7 @@ describe('easeTo globe projection', () => {
test('smoothly sets given padding with duration > 0', async () => { test('smoothly sets given padding with duration > 0', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const promise = camera.once('moveend'); const promise = camera.once('moveend');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -3020,7 +3030,7 @@ describe('easeTo globe projection', () => {
test('pans eastward across the antimeridian', async () => { test('pans eastward across the antimeridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3062,7 +3072,7 @@ describe('easeTo globe projection', () => {
test('pans westward across the antimeridian', async () => { test('pans westward across the antimeridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3162,7 +3172,7 @@ describe('flyTo globe projection', () => {
test('Zoom out from the same position to the same position with animation', async () => { test('Zoom out from the same position to the same position with animation', async () => {
const pos = {lng: 0, lat: 0}; const pos = {lng: 0, lat: 0};
const camera = createCameraGlobe({zoom: 20, center: pos}); const camera = createCameraGlobe({zoom: 20, center: pos});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const promise = camera.once('zoomend'); const promise = camera.once('zoomend');
@ -3241,7 +3251,7 @@ describe('flyTo globe projection', () => {
test('smoothly sets given padding with duration > 0', async () => { test('smoothly sets given padding with duration > 0', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const promise = camera.once('moveend'); const promise = camera.once('moveend');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
@ -3408,7 +3418,7 @@ describe('flyTo globe projection', () => {
camera.on('pitchend', (d) => { pitchended = d.data; }); camera.on('pitchend', (d) => { pitchended = d.data; });
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [100, 0], duration: 10}, eventData); camera.flyTo({center: [100, 0], duration: 10}, eventData);
@ -3455,7 +3465,7 @@ describe('flyTo globe projection', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [100, 0], zoom: 18, duration: 10}); camera.flyTo({center: [100, 0], zoom: 18, duration: 10});
@ -3476,7 +3486,7 @@ describe('flyTo globe projection', () => {
test('pans eastward across the prime meridian', async () => { test('pans eastward across the prime meridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-10, 0]); camera.setCenter([-10, 0]);
let crossedPrimeMeridian; let crossedPrimeMeridian;
@ -3508,7 +3518,7 @@ describe('flyTo globe projection', () => {
test('pans westward across the prime meridian', async () => { test('pans westward across the prime meridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([10, 0]); camera.setCenter([10, 0]);
let crossedPrimeMeridian; let crossedPrimeMeridian;
@ -3540,7 +3550,7 @@ describe('flyTo globe projection', () => {
test('pans eastward across the antimeridian', async () => { test('pans eastward across the antimeridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3572,7 +3582,7 @@ describe('flyTo globe projection', () => {
test('pans westward across the antimeridian', async () => { test('pans westward across the antimeridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3604,7 +3614,7 @@ describe('flyTo globe projection', () => {
test('pans eastward across the antimeridian even if renderWorldCopies: false', async () => { test('pans eastward across the antimeridian even if renderWorldCopies: false', async () => {
const camera = createCameraGlobe({renderWorldCopies: false}); const camera = createCameraGlobe({renderWorldCopies: false});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([170, 0]); camera.setCenter([170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3636,7 +3646,7 @@ describe('flyTo globe projection', () => {
test('pans westward across the antimeridian even if renderWorldCopies: false', async () => { test('pans westward across the antimeridian even if renderWorldCopies: false', async () => {
const camera = createCameraGlobe({renderWorldCopies: false}); const camera = createCameraGlobe({renderWorldCopies: false});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
let crossedAntimeridian; let crossedAntimeridian;
@ -3668,7 +3678,7 @@ describe('flyTo globe projection', () => {
test('jumps back to world 0 when crossing the antimeridian', async () => { test('jumps back to world 0 when crossing the antimeridian', async () => {
const camera = createCameraGlobe(); const camera = createCameraGlobe();
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
camera.setCenter([-170, 0]); camera.setCenter([-170, 0]);
@ -3699,7 +3709,7 @@ describe('flyTo globe projection', () => {
test('peaks at the specified zoom level', async () => { test('peaks at the specified zoom level', async () => {
const camera = createCameraGlobe({zoom: 20}); const camera = createCameraGlobe({zoom: 20});
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
const minZoom = 1; const minZoom = 1;
let zoomed = false; let zoomed = false;
@ -3743,7 +3753,7 @@ describe('flyTo globe projection', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: [12, 34], zoom: 30, duration: 10}); camera.flyTo({center: [12, 34], zoom: 30, duration: 10});
@ -3772,7 +3782,7 @@ describe('flyTo globe projection', () => {
const promise = camera.once('moveend'); const promise = camera.once('moveend');
const stub = vi.spyOn(browser, 'now'); const stub = vi.spyOn(timeControl, 'now');
stub.mockImplementation(() => 0); stub.mockImplementation(() => 0);
camera.flyTo({center: target, zoom: 1, duration: 10}); camera.flyTo({center: target, zoom: 1, duration: 10});

View File

@ -1,6 +1,7 @@
import {extend, wrap, defaultEasing, pick, scaleZoom} from '../util/util'; import {extend, wrap, defaultEasing, pick, scaleZoom} from '../util/util';
import {interpolates} from '@maplibre/maplibre-gl-style-spec'; import {interpolates} from '@maplibre/maplibre-gl-style-spec';
import {browser} from '../util/browser'; import {browser} from '../util/browser';
import {now} from '../util/time_control';
import {LngLat} from '../geo/lng_lat'; import {LngLat} from '../geo/lng_lat';
import {LngLatBounds} from '../geo/lng_lat_bounds'; import {LngLatBounds} from '../geo/lng_lat_bounds';
import Point from '@mapbox/point-geometry'; import Point from '@mapbox/point-geometry';
@ -1069,9 +1070,10 @@ export abstract class Camera extends Evented {
* between old and new values. The map will retain its current values for any * between old and new values. The map will retain its current values for any
* details not specified in `options`. * details not specified in `options`.
* *
* Note: The transition will happen instantly if the user has enabled * !!! note "Reduced Motion"
* the `reduced motion` accessibility feature enabled in their operating system, * The transition will happen instantly if the user has enabled
* unless `options` includes `essential: true`. * the `reduced motion` accessibility feature enabled in their operating system,
* unless `options` includes `essential: true`.
* *
* Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`,
* `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`.
@ -1351,9 +1353,10 @@ export abstract class Camera extends Evented {
* evokes flight. The animation seamlessly incorporates zooming and panning to help * evokes flight. The animation seamlessly incorporates zooming and panning to help
* the user maintain her bearings even after traversing a great distance. * the user maintain her bearings even after traversing a great distance.
* *
* Note: The animation will be skipped, and this will behave equivalently to `jumpTo` * !!! note "Reduced Motion"
* if the user has the `reduced motion` accessibility feature enabled in their operating system, * The animation will be skipped, and this will behave equivalently to `jumpTo`
* unless 'options' includes `essential: true`. * if the user has the `reduced motion` accessibility feature enabled in their operating system,
* unless 'options' includes `essential: true`.
* *
* Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`,
* `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`.
@ -1384,7 +1387,7 @@ export abstract class Camera extends Evented {
flyTo(options: FlyToOptions, eventData?: any): this { flyTo(options: FlyToOptions, eventData?: any): this {
// Fall through to jumpTo if user has set prefers-reduced-motion // Fall through to jumpTo if user has set prefers-reduced-motion
if (!options.essential && browser.prefersReducedMotion) { if (!options.essential && browser.prefersReducedMotion) {
const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'roll', 'elevation']) as CameraOptions; const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'roll', 'elevation', 'padding']) as JumpToOptions;
return this.jumpTo(coercedOptions, eventData); return this.jumpTo(coercedOptions, eventData);
} }
@ -1593,7 +1596,7 @@ export abstract class Camera extends Evented {
frame(1); frame(1);
finish(); finish();
} else { } else {
this._easeStart = browser.now(); this._easeStart = now();
this._easeOptions = options; this._easeOptions = options;
this._onEaseFrame = frame; this._onEaseFrame = frame;
this._onEaseEnd = finish; this._onEaseEnd = finish;
@ -1603,7 +1606,7 @@ export abstract class Camera extends Evented {
// Callback for map._requestRenderFrame // Callback for map._requestRenderFrame
_renderFrameCallback = () => { _renderFrameCallback = () => {
const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1); const t = Math.min((now() - this._easeStart) / this._easeOptions.duration, 1);
this._onEaseFrame(this._easeOptions.easing(t)); this._onEaseFrame(this._easeOptions.easing(t));
// if _stop is called during _onEaseFrame from _fireMoveEvents we should avoid a new _requestRenderFrame, checking it by ensuring _easeFrameId was not deleted // if _stop is called during _onEaseFrame from _fireMoveEvents we should avoid a new _requestRenderFrame, checking it by ensuring _easeFrameId was not deleted

View File

@ -111,10 +111,118 @@ describe('GeolocateControl with no options', () => {
expect(geolocate._geolocateButton.disabled).toBeFalsy(); expect(geolocate._geolocateButton.disabled).toBeFalsy();
}); });
test('error event', async () => { test('error event in waiting active state', async () => {
const geolocate = new GeolocateControl(undefined); const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate); map.addControl(geolocate);
await sleep(0); await sleep(0);
geolocate._watchState = 'WAITING_ACTIVE';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
});
test('error event in active lock state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
geolocate._watchState = 'ACTIVE_LOCK';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
});
test('error event in background state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
geolocate._watchState = 'BACKGROUND';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('BACKGROUND_ERROR');
});
test('error event in active error state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
geolocate._watchState = 'ACTIVE_ERROR';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
});
test('error event in background error state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
geolocate._watchState = 'BACKGROUND_ERROR';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('BACKGROUND_ERROR');
});
test('error event in off state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
geolocate._watchState = 'OFF';
const click = new window.Event('click');
const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.sendError({code: 2, message: 'error message'});
const error = await errorPromise;
expect(error.code).toBe(2);
expect(error.message).toBe('error message');
expect(geolocate._watchState).toBe('OFF');
});
test('error event when trackUserLocation is false', async () => {
const geolocate = new GeolocateControl({trackUserLocation: false});
map.addControl(geolocate);
await sleep(0);
const click = new window.Event('click'); const click = new window.Event('click');
const errorPromise = geolocate.once('error'); const errorPromise = geolocate.once('error');
geolocate._geolocateButton.dispatchEvent(click); geolocate._geolocateButton.dispatchEvent(click);
@ -124,6 +232,7 @@ describe('GeolocateControl with no options', () => {
expect(error.code).toBe(2); expect(error.code).toBe(2);
expect(error.message).toBe('error message'); expect(error.message).toBe('error message');
expect(geolocate._watchState).toBeUndefined();
}); });
test('does not throw if removed quickly', () => { test('does not throw if removed quickly', () => {
@ -137,6 +246,26 @@ describe('GeolocateControl with no options', () => {
map.removeControl(geolocate); map.removeControl(geolocate);
}); });
test('outofmaxbounds event in waiting active state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
map.setMaxBounds([[0, 0], [10, 10]]);
geolocate._watchState = 'WAITING_ACTIVE';
const click = new window.Event('click');
const promise = geolocate.once('outofmaxbounds');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4});
const position = await promise;
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
expect(position.coords.latitude).toBe(10);
expect(position.coords.longitude).toBe(20);
expect(position.coords.accuracy).toBe(3);
expect(position.timestamp).toBe(4);
});
test('outofmaxbounds event in active lock state', async () => { test('outofmaxbounds event in active lock state', async () => {
const geolocate = new GeolocateControl(undefined); const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate); map.addControl(geolocate);
@ -178,6 +307,85 @@ describe('GeolocateControl with no options', () => {
expect(position.timestamp).toBe(4); expect(position.timestamp).toBe(4);
}); });
test('outofmaxbounds event in active error state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
map.setMaxBounds([[0, 0], [10, 10]]);
geolocate._watchState = 'ACTIVE_ERROR';
const click = new window.Event('click');
const promise = geolocate.once('outofmaxbounds');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4});
const position = await promise;
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
expect(position.coords.latitude).toBe(10);
expect(position.coords.longitude).toBe(20);
expect(position.coords.accuracy).toBe(3);
expect(position.timestamp).toBe(4);
});
test('outofmaxbounds event in background error state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
map.setMaxBounds([[0, 0], [10, 10]]);
geolocate._watchState = 'BACKGROUND_ERROR';
const click = new window.Event('click');
const promise = geolocate.once('outofmaxbounds');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4});
const position = await promise;
expect(geolocate._watchState).toBe('BACKGROUND_ERROR');
expect(position.coords.latitude).toBe(10);
expect(position.coords.longitude).toBe(20);
expect(position.coords.accuracy).toBe(3);
expect(position.timestamp).toBe(4);
});
test('outofmaxbounds event in off state', async () => {
const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate);
await sleep(0);
map.setMaxBounds([[0, 0], [10, 10]]);
geolocate._watchState = 'OFF';
const click = new window.Event('click');
const promise = geolocate.once('outofmaxbounds');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4});
const position = await promise;
expect(geolocate._watchState).toBe('OFF');
expect(position.coords.latitude).toBe(10);
expect(position.coords.longitude).toBe(20);
expect(position.coords.accuracy).toBe(3);
expect(position.timestamp).toBe(4);
});
test('outofmaxbounds event when trackUserLocation = false', async () => {
const geolocate = new GeolocateControl({trackUserLocation: false});
map.addControl(geolocate);
await sleep(0);
map.setMaxBounds([[0, 0], [10, 10]]);
const click = new window.Event('click');
const promise = geolocate.once('outofmaxbounds');
geolocate._geolocateButton.dispatchEvent(click);
geolocation.send({latitude: 10, longitude: 20, accuracy: 3, timestamp: 4});
const position = await promise;
expect(geolocate._watchState).toBeUndefined();
expect(position.coords.latitude).toBe(10);
expect(position.coords.longitude).toBe(20);
expect(position.coords.accuracy).toBe(3);
expect(position.timestamp).toBe(4);
});
test('geolocate event', async () => { test('geolocate event', async () => {
const geolocate = new GeolocateControl(undefined); const geolocate = new GeolocateControl(undefined);
map.addControl(geolocate); map.addControl(geolocate);

Some files were not shown because too many files have changed in this diff Show More