maplibre-gl 6.6.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/build/generate-unicode-data.ts +83 -0
  2. package/dist/maplibre-gl-dev.mjs +480 -80
  3. package/dist/maplibre-gl-dev.mjs.map +1 -1
  4. package/dist/maplibre-gl-shared-dev.mjs +1578 -983
  5. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  6. package/dist/maplibre-gl-shared.mjs +2 -2
  7. package/dist/maplibre-gl-shared.mjs.map +1 -1
  8. package/dist/maplibre-gl-worker-dev.mjs +7 -6
  9. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  10. package/dist/maplibre-gl-worker.mjs +2 -2
  11. package/dist/maplibre-gl-worker.mjs.map +1 -1
  12. package/dist/maplibre-gl.d.ts +306 -66
  13. package/dist/maplibre-gl.mjs +36 -36
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +23 -18
  16. package/src/data/bucket/symbol_bucket.test.ts +45 -12
  17. package/src/data/bucket/symbol_bucket.ts +39 -18
  18. package/src/data/bucket.ts +11 -3
  19. package/src/data/feature_index.ts +35 -6
  20. package/src/geo/projection/mercator_transform.test.ts +24 -4
  21. package/src/geo/projection/mercator_transform.ts +1 -1
  22. package/src/geo/projection/vertical_perspective_transform.ts +1 -1
  23. package/src/render/font_face_manager.test.ts +295 -0
  24. package/src/render/font_face_manager.ts +249 -0
  25. package/src/render/glyph_atlas.ts +3 -7
  26. package/src/render/glyph_manager.test.ts +251 -94
  27. package/src/render/glyph_manager.ts +195 -52
  28. package/src/render/terrain.test.ts +36 -0
  29. package/src/render/terrain.ts +17 -7
  30. package/src/source/geojson_worker_source.test.ts +4 -53
  31. package/src/source/image_source.test.ts +53 -0
  32. package/src/source/image_source.ts +24 -2
  33. package/src/source/vector_tile_worker_source.test.ts +46 -38
  34. package/src/source/vector_tile_worker_source.ts +6 -1
  35. package/src/source/worker_source.ts +1 -5
  36. package/src/source/worker_tile.test.ts +6 -22
  37. package/src/source/worker_tile.ts +8 -5
  38. package/src/style/style.test.ts +92 -0
  39. package/src/style/style.ts +67 -3
  40. package/src/style/style_image.ts +5 -1
  41. package/src/style/style_layer/symbol_style_layer.ts +7 -7
  42. package/src/style/style_layer/symbol_style_layer_properties.g.ts +3 -3
  43. package/src/symbol/shaping.test.ts +436 -2
  44. package/src/symbol/shaping.ts +298 -113
  45. package/src/symbol/symbol_layout.ts +1 -1
  46. package/src/symbol/tagged_string.test.ts +29 -6
  47. package/src/symbol/tagged_string.ts +151 -89
  48. package/src/tile/terrain_tile_manager.test.ts +23 -0
  49. package/src/tile/terrain_tile_manager.ts +6 -1
  50. package/src/ui/camera.test.ts +6 -5
  51. package/src/ui/camera.ts +3 -3
  52. package/src/ui/control/navigation_control.test.ts +32 -0
  53. package/src/ui/control/navigation_control.ts +3 -3
  54. package/src/ui/map.ts +87 -21
  55. package/src/ui/map_tests/map_render.test.ts +139 -1
  56. package/src/ui/map_tests/map_style.test.ts +17 -0
  57. package/src/ui/map_tests/map_terrain.test.ts +60 -1
  58. package/src/ui/map_tests/map_webgl.test.ts +41 -11
  59. package/src/ui/map_tests/map_zoom.test.ts +11 -0
  60. package/src/util/actor_messages.ts +3 -6
  61. package/src/util/gpu_initialization_error.ts +2 -1
  62. package/src/util/graphemes.test.ts +79 -0
  63. package/src/util/graphemes.ts +79 -0
  64. package/src/util/test/util.ts +46 -1
  65. package/src/util/unicode_properties.g.ts +28 -0
  66. package/src/util/util.ts +6 -2
  67. package/src/webgl/draw/draw_symbol.ts +1 -1
  68. package/src/style/load_glyph_range.test.ts +0 -74
  69. package/src/style/load_glyph_range.ts +0 -32
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * MapLibre GL JS
3
- * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.6.0/LICENSE.txt
3
+ * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.7.0/LICENSE.txt
4
4
  */
5
- import { $ as projectToWorldCoordinates, $n as createVec4f64, $r as slerp, $t as Properties, A as evaluateSizeForFeature, Ai as perspective, An as getReferrer, Ar as readImageUsingVideoFrame, At as polygonIntersectsPolygon, Bi as invert, Bn as throwIfAborted, Br as warnOnce, Bt as UniformMatrix4f, C as isCustomStyleLayer, Ci as create$1, Cn as ErrorEvent, Cr as mod, Ct as isHillshadeStyleLayer, D as SymbolBucket, Di as invert$1, Dn as GLOBAL_DISPATCHER_ID, Dr as pointPlaneSignedDistance, Dt as AlphaImage, E as isSymbolStyleLayer, Ei as identity, En as AJAXError, Er as pick, Et as renderColorRamp, F as ImagePosition, Fi as translate, Fn as getProtocol, Fr as sphericalToCartesian, Ft as Uniform3f, G as isFillExtrusionStyleLayer, Gn as arrayBufferToImageBitmap, Gr as create$3, Gt as Pos3dArray, H as isLineStyleLayer, Hi as isOffscreenCanvasDistorted, Hn as MAX_VALID_LATITUDE, Hr as zoomScale, Ht as CollisionBoxArray, I as potpack, Ii as create$4, In as removeProtocol, Ir as subscribe, It as Uniform4f, J as cameraDirectionFromPitchBearing, Jn as clone$2, Jr as sqrLen, Jt as RasterBoundsArray, Kn as bezier, Kr as length, Kt as PosArray, L as isStyleImageWebGLData, Li as fromRotation, Ln as config, Lr as threePlaneIntersection, Lt as UniformColor, Mi as rotateY$1, Mn as makeRequest, Mr as rollPitchBearingEqual, Mt as Uniform1f, N as getAnchorAlignment, Ni as rotateZ, Nn as sameOrigin, Nr as rollPitchBearingToQuat, Nt as Uniform1i, O as addDynamicAttributes, Oi as multiply, On as getArrayBuffer, Or as radiansToDegrees, Ot as RGBAImage, Pi as scale, Pn as addProtocol, Pr as scaleZoom, Pt as Uniform2f, Q as maxMercatorHorizonAngle, Qn as createVec3f64, Qr as multiply$1, Qt as DataConstantProperty, R as renderStyleImage, Ri as create$2, Rn as AbortError, Rr as translatePosition, Rt as UniformColorArray, S as createStyleLayer, Si as copy, Sn as latest, Sr as mapObject, St as Texture, T as isBackgroundStyleLayer, Ti as fromScaling, Tn as Evented, Tr as parseCacheControl, Tt as isHeatmapStyleLayer, Ui as offscreenCanvasSupported, Un as angleToRotateBetweenVectors2D, Ur as pixelsToTileUnits, Ut as CollisionCircleLayoutArray, V as collisionCircleLayout, Vi as rotate, Vr as wrap, Vt as SegmentVector, Wi as Point, Wn as arrayBufferToImage, Wr as EXTENT, Wt as LineStripIndexArray, X as cameraMercatorCoordinateFromCenterAndRotation, Xn as createIdentityMat4f64, Xr as fromEuler, Xt as createLayout, Y as cameraMercatorCoordinate, Yn as createIdentityMat4f32, Yr as zero, Yt as TriangleIndexArray, Z as getMercatorHorizon, Zn as createMat4f64, Zr as fromValues, Zt as isRasterStyleLayer, _ as UnwrappedTileID, _i as transformMat3, _n as diff, _r as isSafari, _t as SOUTH_POLE_Y, a as clipLine, ai as cross, an as ZoomHistory, ar as ensureError, at as lngFromMercatorX, b as isInBoundsForZoomLngLat, bi as zero$1, br as isWorker, bt as isColorReliefStyleLayer, ci as length$1, cn as emitValidationErrors, cr as filterObject, ct as mercatorZfromAltitude, di as rotateX$1, dn as validateStyleAndEmit, dr as getAngleDelta, ei as mul, en as TRANSITION_SUFFIX, er as deepEqual, et as tileCoordinatesToMercatorCoordinates, f as GEOJSON_TILE_LAYER_NAME, fi as rotateY, fn as Color, fr as getEdgeTiles, ft as EXTENT_BOUNDS, g as OverscaledTileID, gi as sub, gn as derefLayers, gr as isPointableEvent, gt as NORTH_POLE_Y, h as CanonicalTileID, hi as scaleAndAdd, hr as isImageBitmap, ii as clone, in as codePointUsesLocalIdeographFontFamily, ir as distanceOfAnglesRadians, it as latFromMercatorY, j as evaluateSizeForZoom, ji as rotateX, jn as getVideo, jr as remapSaturate, jt as toEvaluationFeature, k as getOverlapMode, ki as ortho, kn as getJSON, kr as rayPlaneIntersection, kt as isCircleStyleLayer, li as negate, ln as validateAndEmit, lr as findLineIntersection, lt as LngLat, mi as scale$2, mn as ValidationError, mr as getRollPitchBearing, mt as isFillStyleLayer, ni as transformMat4, nn as EvaluationParameters, nr as degreesToRadians, nt as MercatorCoordinate, oi as dot, or as evaluateZoomSnap, ot as mercatorXfromLng, pi as rotateZ$1, pn as ProjectionDefinition, pt as Bounds, q as calculateTileMatrix, qn as clamp, qr as scale$1, qt as QuadTriangleArray, r as TextAnchorEnum, ri as add, rr as differenceOfAnglesDegrees, rt as altitudeFromMercatorZ, s as TileCache, si as len, sn as SPEC_SOURCE_TYPES, sr as extend, st as mercatorYfromLat, t as getAnchorJustification, ti as scale$3, tn as Transitionable, tr as defaultEasing, tt as unprojectFromWorldCoordinates, u as GeoJSONFeature, ui as normalize, un as validateStyle, ur as getAABB, ut as earthRadius, v as calculateTileKey, vi as transformMat4$1, vn as emptyStyle, vr as isTouchableEvent, vt as SubdivisionGranularityExpression, w as validateCustomStyleLayer, wi as equals, wn as Event, wr as nextPowerOfTwo, wt as HEATMAP_FULL_RENDER_FBO_KEY, x as Actor, xi as clone$1, xn as interpolateFactory, xr as lerp, y as compareTileId, yi as transformQuat, yn as featureFilter, yr as isTouchableOrPointableType, yt as SubdivisionGranularitySetting, z as parseGlyphPbf, zi as determinant, zn as isAbortError, zr as uniqueId, zt as UniformFloatArray } from "./maplibre-gl-shared-dev.mjs";
5
+ import { $ as maxMercatorHorizonAngle, $n as createVec3f64, $r as multiply$1, $t as DataConstantProperty, A as evaluateSizeForFeature, Ai as multiply, An as getJSON, Ar as rayPlaneIntersection, At as isCircleStyleLayer, Bi as create$2, Bn as isAbortError, Br as uniqueId, Bt as UniformFloatArray, C as isCustomStyleLayer, Ci as copy, Cn as latest, Cr as mapObject, Ct as Texture, D as SymbolBucket, Di as fromScaling, Dn as AJAXError, Dr as pick, Dt as renderColorRamp, E as isSymbolStyleLayer, Ei as exactEquals, En as Evented, Er as parseCacheControl, Et as isHeatmapStyleLayer, F as ImagePosition, Fi as rotateZ, Fn as addProtocol, Fr as scaleZoom, Ft as Uniform2f, Gi as offscreenCanvasSupported, Gn as arrayBufferToImage, Gr as EXTENT, Gt as LineStripIndexArray, H as collisionCircleLayout, Hi as invert, Hr as wrap, Ht as SegmentVector, I as potpack, Ii as scale, In as getProtocol, Ir as sphericalToCartesian, It as Uniform3f, J as calculateTileMatrix, Jn as clamp, Jr as scale$1, Jt as QuadTriangleArray, K as isFillExtrusionStyleLayer, Ki as Point, Kn as arrayBufferToImageBitmap, Kr as create$3, Kt as Pos3dArray, L as isStyleImageWebGLData, Li as translate, Ln as removeProtocol, Lr as subscribe, Lt as Uniform4f, Mi as perspective, Mn as getVideo, Mr as remapSaturate, Mt as toEvaluationFeature, N as getAnchorAlignment, Ni as rotateX, Nn as makeRequest, Nr as rollPitchBearingEqual, Nt as Uniform1f, O as addDynamicAttributes, Oi as identity, On as GLOBAL_DISPATCHER_ID, Or as pointPlaneSignedDistance, Ot as AlphaImage, Pi as rotateY$1, Pn as sameOrigin, Pr as rollPitchBearingToQuat, Pt as Uniform1i, Q as getMercatorHorizon, Qn as createMat4f64, Qr as fromValues, Qt as isRasterStyleLayer, R as renderStyleImage, Ri as create$4, Rn as config, Rr as threePlaneIntersection, Rt as UniformColor, S as createStyleLayer, Si as clone$1, Sn as derefLayers, Sr as lerp, T as isBackgroundStyleLayer, Ti as equals, Tn as Event, Tr as nextPowerOfTwo, Tt as HEATMAP_FULL_RENDER_FBO_KEY, U as isLineStyleLayer, Ui as rotate, Un as MAX_VALID_LATITUDE, Ur as zoomScale, Ut as CollisionBoxArray, V as isCluster, Vi as determinant, Vn as throwIfAborted, Vr as warnOnce, Vt as UniformMatrix4f, Wi as isOffscreenCanvasDistorted, Wn as angleToRotateBetweenVectors2D, Wr as pixelsToTileUnits, Wt as CollisionCircleLayoutArray, X as cameraMercatorCoordinate, Xn as createIdentityMat4f32, Xr as zero, Xt as TriangleIndexArray, Y as cameraDirectionFromPitchBearing, Yn as clone$2, Yr as sqrLen, Yt as RasterBoundsArray, Z as cameraMercatorCoordinateFromCenterAndRotation, Zn as createIdentityMat4f64, Zr as fromEuler, Zt as createLayout, _ as UnwrappedTileID, _i as sub, _n as interpolateFactory, _r as isPointableEvent, _t as NORTH_POLE_Y, a as clipLine, ai as clone, an as codePointUsesLocalIdeographFontFamily, ar as distanceOfAnglesRadians, at as latFromMercatorY, b as isInBoundsForZoomLngLat, bi as transformQuat, bn as ValidationError, br as isTouchableOrPointableType, bt as SubdivisionGranularitySetting, ci as len, cn as SPEC_SOURCE_TYPES, cr as extend, ct as mercatorYfromLat, di as normalize, dn as validateStyle, dr as getAABB, dt as earthRadius, ei as slerp, en as Properties, er as createVec4f64, et as projectToWorldCoordinates, f as GEOJSON_TILE_LAYER_NAME, fi as rotateX$1, fn as validateStyleAndEmit, fr as getAngleDelta, g as OverscaledTileID, gi as scaleAndAdd, gr as isImageBitmap, h as CanonicalTileID, hi as scale$2, hn as featureFilter, hr as getRollPitchBearing, ht as isFillStyleLayer, ii as add, ir as differenceOfAnglesDegrees, it as altitudeFromMercatorZ, j as evaluateSizeForZoom, ji as ortho, jn as getReferrer, jr as readImageUsingVideoFrame, jt as polygonIntersectsPolygon, k as getOverlapMode, ki as invert$1, kn as getArrayBuffer, kr as radiansToDegrees, kt as RGBAImage, li as length$1, ln as emitValidationErrors, lr as filterObject, lt as mercatorZfromAltitude, mi as rotateZ$1, mt as Bounds, ni as scale$3, nn as Transitionable, nr as defaultEasing, nt as unprojectFromWorldCoordinates, oi as cross, on as ZoomHistory, or as ensureError, ot as lngFromMercatorX, pi as rotateY, pn as emptyStyle, pr as getEdgeTiles, pt as EXTENT_BOUNDS, qn as bezier, qr as length, qt as PosArray, r as TextAnchorEnum, ri as transformMat4, rn as EvaluationParameters, rr as degreesToRadians, rt as MercatorCoordinate, s as TileCache, si as dot, sr as evaluateZoomSnap, st as mercatorXfromLng, t as getAnchorJustification, ti as mul, tn as TRANSITION_SUFFIX, tr as deepEqual, tt as tileCoordinatesToMercatorCoordinates, u as GeoJSONFeature, ui as negate, un as validateAndEmit, ur as findLineIntersection, ut as LngLat, v as calculateTileKey, vi as transformMat3, vn as ProjectionDefinition, vr as isSafari, vt as SOUTH_POLE_Y, w as validateCustomStyleLayer, wi as create$1, wn as ErrorEvent, wr as mod, wt as isHillshadeStyleLayer, x as Actor, xi as zero$1, xn as diff, xr as isWorker, xt as isColorReliefStyleLayer, y as compareTileId, yi as transformMat4$1, yn as Color, yr as isTouchableEvent, yt as SubdivisionGranularityExpression, z as parseGlyphPbf, zi as fromRotation, zn as AbortError, zr as translatePosition, zt as UniformColorArray } from "./maplibre-gl-shared-dev.mjs";
6
6
  //#region package.json
7
- var version$2 = "6.6.0";
7
+ var version$2 = "6.7.0";
8
8
  //#endregion
9
9
  //#region src/util/browser.ts
10
10
  let linkEl;
@@ -1165,17 +1165,183 @@ var PatternAtlas = class {
1165
1165
  }
1166
1166
  };
1167
1167
  //#endregion
1168
- //#region src/style/load_glyph_range.ts
1169
- async function loadGlyphRange(fontstack, range, urlTemplate, requestManager) {
1170
- const begin = range * 256;
1171
- const end = begin + 255;
1172
- const request = await requestManager.transformRequest(urlTemplate.replace("{fontstack}", fontstack).replace("{range}", `${begin}-${end}`), "Glyphs");
1173
- const response = await getArrayBuffer(request, new AbortController());
1174
- if (!response?.data) throw new Error(`Could not load glyph range. range: ${range}, ${begin}-${end}`);
1175
- const glyphs = {};
1176
- for (const glyph of parseGlyphPbf(response.data)) glyphs[glyph.id] = glyph;
1177
- return glyphs;
1168
+ //#region src/render/font_face_manager.ts
1169
+ const MAX_CODE_POINT = 1114111;
1170
+ /**
1171
+ * What a font file covers when the style does not narrow it down with `unicode-range`.
1172
+ */
1173
+ const DEFAULT_UNICODE_RANGE = {
1174
+ start: 0,
1175
+ end: MAX_CODE_POINT
1176
+ };
1177
+ /**
1178
+ * Makes the family names unique across every map on the page, since `document.fonts` is shared.
1179
+ */
1180
+ let nextFamilyId = 0;
1181
+ /**
1182
+ * Parses one `unicode-range` entry, in the CSS grammar the style specification borrows: `U+A5`,
1183
+ * `U+0-10FFFF` or `U+4??`.
1184
+ * @param value - a single `unicode-range` entry
1185
+ * @returns the codepoints it covers, or `null` if it is not a range this can make sense of
1186
+ */
1187
+ function parseUnicodeRange(value) {
1188
+ const wildcard = /^u\+([0-9a-f]*)(\?+)$/i.exec(value);
1189
+ if (wildcard) {
1190
+ const [, prefix, questionMarks] = wildcard;
1191
+ if (prefix.length + questionMarks.length > 6) return null;
1192
+ return clampRange(parseInt(`${prefix}${"0".repeat(questionMarks.length)}`, 16), parseInt(`${prefix}${"f".repeat(questionMarks.length)}`, 16));
1193
+ }
1194
+ const explicit = /^u\+([0-9a-f]{1,6})(?:-([0-9a-f]{1,6}))?$/i.exec(value);
1195
+ if (!explicit) return null;
1196
+ const start = parseInt(explicit[1], 16);
1197
+ return clampRange(start, explicit[2] === void 0 ? start : parseInt(explicit[2], 16));
1198
+ }
1199
+ function clampRange(start, end) {
1200
+ if (start > end || start > MAX_CODE_POINT) return null;
1201
+ return {
1202
+ start,
1203
+ end: Math.min(end, MAX_CODE_POINT)
1204
+ };
1205
+ }
1206
+ /**
1207
+ * Whether the style lets this file cover the given codepoint.
1208
+ */
1209
+ function covers(face, codePoint) {
1210
+ return face.ranges.some(({ start, end }) => codePoint >= start && codePoint <= end);
1178
1211
  }
1212
+ /**
1213
+ * Keeps the font files a style declares in its
1214
+ * [`font-faces`](https://maplibre.org/maplibre-style-spec/root/#font-faces) property, and hands the
1215
+ * {@link GlyphManager} the CSS family to draw a given codepoint with.
1216
+ *
1217
+ * The files go to the browser's CSS Font Loading API, so any format it can render text with works.
1218
+ * Each is registered under a family name of our own making, so that a style cannot restyle the
1219
+ * surrounding page and a codepoint can be pinned to one file rather than to whatever font matching
1220
+ * picks.
1221
+ *
1222
+ * Rasterizing happens a grapheme cluster at a time, so a letter reaches the browser's text engine
1223
+ * with its marks. Shaping across cluster boundaries still needs the RTL text plugin.
1224
+ */
1225
+ var FontFaceManager = class {
1226
+ constructor(requestManager) {
1227
+ this.requestManager = requestManager;
1228
+ this._faces = {};
1229
+ this._registered = /* @__PURE__ */ new Set();
1230
+ }
1231
+ /**
1232
+ * Replaces the declared font faces. Nothing is downloaded here: each file waits until a
1233
+ * codepoint it covers is actually drawn.
1234
+ */
1235
+ setFontFaces(fontFaces) {
1236
+ this._unregisterAll();
1237
+ this._faces = {};
1238
+ for (const [fontName, declaration] of Object.entries(fontFaces ?? {})) {
1239
+ const declarations = Array.isArray(declaration) ? declaration : [declaration];
1240
+ this._faces[fontName] = declarations.map((entry) => this._declareFontFace(fontName, entry)).filter((face) => face !== null);
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Whether the style declared any font face at all, so that the common case of a style without
1245
+ * `font-faces` costs nothing.
1246
+ */
1247
+ hasFontFaces() {
1248
+ return Object.keys(this._faces).length > 0;
1249
+ }
1250
+ /**
1251
+ * Finds the font file to draw a codepoint with: each name of the `text-font` stack in turn, and
1252
+ * within a name each declared file, until one covers it. A file that fails to load is skipped,
1253
+ * the specification asking for unsupported fonts to be ignored.
1254
+ *
1255
+ * @returns the CSS family to draw with, or `null` to leave the codepoint to the `glyphs` URL
1256
+ */
1257
+ async getFontFamily(fontStack, codePoint) {
1258
+ for (const fontName of fontStack.split(",")) for (const face of this._faces[fontName.trim()] ?? []) {
1259
+ if (!covers(face, codePoint)) continue;
1260
+ face.loaded ??= this._loadFontFace(face);
1261
+ if (await face.loaded) return face.family;
1262
+ }
1263
+ return null;
1264
+ }
1265
+ /**
1266
+ * Turns one declaration into a face to draw with, or `null` if there is nothing usable in it.
1267
+ *
1268
+ * A bare URL is the same as a face that names no `unicode-range`: it covers every codepoint.
1269
+ */
1270
+ _declareFontFace(fontName, declaration) {
1271
+ const face = typeof declaration === "string" ? { url: declaration } : declaration;
1272
+ if (typeof face?.url !== "string") {
1273
+ warnOnce(`Ignoring the font face declared for "${fontName}": it has no URL.`);
1274
+ return null;
1275
+ }
1276
+ const family = `maplibre-gl-font-face-${nextFamilyId++}`;
1277
+ const unicodeRange = face["unicode-range"];
1278
+ if (!unicodeRange?.length) return {
1279
+ url: face.url,
1280
+ ranges: [DEFAULT_UNICODE_RANGE],
1281
+ family
1282
+ };
1283
+ const ranges = [];
1284
+ for (const entry of unicodeRange) {
1285
+ const range = parseUnicodeRange(entry);
1286
+ if (!range) {
1287
+ warnOnce(`Ignoring the unicode range "${entry}" of the font face at ${face.url}: it is not a valid range.`);
1288
+ continue;
1289
+ }
1290
+ ranges.push(range);
1291
+ }
1292
+ if (!ranges.length) return null;
1293
+ return {
1294
+ url: face.url,
1295
+ ranges,
1296
+ family
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Downloads a declared file and hands it to the browser, reporting whether it can be drawn with.
1301
+ * A failure is not an error: its codepoints fall through to the next file, then to `glyphs`.
1302
+ */
1303
+ async _loadFontFace(face) {
1304
+ if (typeof FontFace === "undefined" || typeof document === "undefined" || !document.fonts) {
1305
+ warnOnce(`Ignoring the font face at ${face.url}: this environment has no CSS Font Loading API.`);
1306
+ return false;
1307
+ }
1308
+ let fontFace;
1309
+ try {
1310
+ fontFace = new FontFace(face.family, await this._downloadFontFile(face.url));
1311
+ if (!Object.values(this._faces).some((faces) => faces.includes(face))) return false;
1312
+ document.fonts.add(fontFace);
1313
+ this._registered.add(fontFace);
1314
+ await fontFace.load();
1315
+ return true;
1316
+ } catch (e) {
1317
+ if (fontFace) this._unregister(fontFace);
1318
+ warnOnce(`Ignoring the font face at ${face.url}: ${ensureError(e).message}`);
1319
+ return false;
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Downloads a font file, giving the map's `transformRequest` a chance to rewrite the request the
1324
+ * same way it gets one for a glyph range.
1325
+ */
1326
+ async _downloadFontFile(url) {
1327
+ const request = await this.requestManager.transformRequest(url, "Glyphs");
1328
+ const response = await getArrayBuffer(request, new AbortController());
1329
+ if (!response?.data) throw new Error(`the response was empty for the font file at ${url}`);
1330
+ return response.data;
1331
+ }
1332
+ _unregister(fontFace) {
1333
+ document.fonts?.delete(fontFace);
1334
+ this._registered.delete(fontFace);
1335
+ }
1336
+ _unregisterAll() {
1337
+ for (const fontFace of this._registered) document.fonts?.delete(fontFace);
1338
+ this._registered.clear();
1339
+ }
1340
+ destroy() {
1341
+ this._unregisterAll();
1342
+ this._faces = {};
1343
+ }
1344
+ };
1179
1345
  //#endregion
1180
1346
  //#region node_modules/@mapbox/tiny-sdf/index.js
1181
1347
  const INF = 0x56bc75e2d63100000;
@@ -1305,22 +1471,38 @@ const defaultGenericFontFamily = "sans-serif";
1305
1471
  * Client-generated glyphs are rendered at 2× because CJK glyphs are more detailed than others.
1306
1472
  */
1307
1473
  const textureScale = 2;
1308
- var GlyphManager = class GlyphManager {
1309
- static {
1310
- this.loadGlyphRange = loadGlyphRange;
1311
- }
1312
- static {
1313
- this.TinySDF = TinySDF;
1314
- }
1315
- constructor(requestManager, localIdeographFontFamily, lang) {
1474
+ const defaultCreateRasterizer = (options, padding) => {
1475
+ const tinySDF = new TinySDF(options);
1476
+ tinySDF.buffer = padding;
1477
+ return tinySDF;
1478
+ };
1479
+ /**
1480
+ * How wide a grapheme cluster may be drawn, in multiples of the font size.
1481
+ *
1482
+ * TinySDF sizes its canvas for a single character and cuts off the rest, but a cluster is a whole
1483
+ * syllable: Burmese `လား` is nearly twice the font size. Three ems fits the ones that occur.
1484
+ */
1485
+ const clusterEmsWide = 3;
1486
+ var GlyphManager = class {
1487
+ constructor(requestManager, localIdeographFontFamily, lang, createRasterizer = defaultCreateRasterizer) {
1316
1488
  this.requestManager = requestManager;
1317
1489
  this.localIdeographFontFamily = localIdeographFontFamily;
1318
1490
  this.entries = {};
1319
1491
  this.lang = lang;
1492
+ this.fontFaceManager = new FontFaceManager(requestManager);
1493
+ this.createRasterizer = createRasterizer;
1320
1494
  }
1321
1495
  setURL(url) {
1322
1496
  this.url = url;
1323
1497
  }
1498
+ /**
1499
+ * Replaces the font files the style declares in its `font-faces` property, dropping every glyph
1500
+ * drawn with the previous ones.
1501
+ */
1502
+ setFontFaces(fontFaces) {
1503
+ this.fontFaceManager.setFontFaces(fontFaces);
1504
+ this.entries = {};
1505
+ }
1324
1506
  async getGlyphs(glyphs) {
1325
1507
  const glyphsPromises = [];
1326
1508
  for (const stack in glyphs) for (const id of glyphs[stack]) glyphsPromises.push(this._getAndCacheGlyphsPromise(stack, id));
@@ -1336,6 +1518,14 @@ var GlyphManager = class GlyphManager {
1336
1518
  }
1337
1519
  return result;
1338
1520
  }
1521
+ /**
1522
+ * Gets one glyph, asked for by grapheme cluster so that a letter and its marks are drawn as the
1523
+ * one shape they are written as.
1524
+ *
1525
+ * Only a file the style pinned with `font-faces` can draw a cluster -- a glyphs URL serves
1526
+ * codepoints -- so where none covers it this returns nothing and layout falls back to codepoints.
1527
+ * For a single codepoint a declared file still wins over the glyphs URL and the local fallbacks.
1528
+ */
1339
1529
  async _getAndCacheGlyphsPromise(stack, id) {
1340
1530
  this.entries[stack] ??= {
1341
1531
  glyphs: {},
@@ -1349,7 +1539,25 @@ var GlyphManager = class GlyphManager {
1349
1539
  id,
1350
1540
  glyph
1351
1541
  };
1352
- if (!this.url || this._charUsesLocalIdeographFontFamily(id)) {
1542
+ const codePoint = id.codePointAt(0);
1543
+ const fontFaceFamily = this.fontFaceManager.hasFontFaces() ? await this.fontFaceManager.getFontFamily(stack, codePoint) : null;
1544
+ if (fontFaceFamily) {
1545
+ glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id, fontFaceFamily);
1546
+ return {
1547
+ stack,
1548
+ id,
1549
+ glyph
1550
+ };
1551
+ }
1552
+ if (isCluster(id)) {
1553
+ glyph = entry.glyphs[id] = null;
1554
+ return {
1555
+ stack,
1556
+ id,
1557
+ glyph
1558
+ };
1559
+ }
1560
+ if (!this.url || this._charUsesLocalIdeographFontFamily(codePoint)) {
1353
1561
  glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id);
1354
1562
  return {
1355
1563
  stack,
@@ -1359,27 +1567,34 @@ var GlyphManager = class GlyphManager {
1359
1567
  }
1360
1568
  return await this._downloadAndCacheRangePromise(stack, id);
1361
1569
  }
1570
+ /**
1571
+ * Gets a glyph from the server-side cache, downloading the PBF range it falls in if need be.
1572
+ *
1573
+ * Only reached for a single codepoint. What comes back is keyed by codepoint, as the file is,
1574
+ * and is cached by cluster -- which for one codepoint is the character itself.
1575
+ */
1362
1576
  async _downloadAndCacheRangePromise(stack, id) {
1577
+ const codePoint = id.codePointAt(0);
1363
1578
  const entry = this.entries[stack];
1364
- const range = Math.floor(id / 256);
1579
+ const range = Math.floor(codePoint / 256);
1365
1580
  if (entry.ranges[range]) return {
1366
1581
  stack,
1367
1582
  id,
1368
1583
  glyph: null
1369
1584
  };
1370
- entry.requests[range] ||= GlyphManager.loadGlyphRange(stack, range, this.url, this.requestManager);
1585
+ entry.requests[range] ||= this._loadGlyphRange(stack, range);
1371
1586
  try {
1372
1587
  const response = await entry.requests[range];
1373
- for (const id in response) entry.glyphs[+id] = response[+id];
1588
+ for (const responseId in response) entry.glyphs[String.fromCodePoint(+responseId)] = response[+responseId];
1374
1589
  entry.ranges[range] = true;
1375
1590
  return {
1376
1591
  stack,
1377
1592
  id,
1378
- glyph: response[id] || null
1593
+ glyph: response[codePoint] || null
1379
1594
  };
1380
1595
  } catch (e) {
1381
1596
  const glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id);
1382
- this._warnOnMissingGlyphRange(glyph, range, id, ensureError(e));
1597
+ this._warnOnMissingGlyphRange(glyph, range, codePoint, ensureError(e));
1383
1598
  return {
1384
1599
  stack,
1385
1600
  id,
@@ -1387,6 +1602,19 @@ var GlyphManager = class GlyphManager {
1387
1602
  };
1388
1603
  }
1389
1604
  }
1605
+ /**
1606
+ * Downloads one range of 256 codepoints from the glyphs URL and parses the glyphs out of it.
1607
+ */
1608
+ async _loadGlyphRange(fontstack, range) {
1609
+ const begin = range * 256;
1610
+ const end = begin + 255;
1611
+ const request = await this.requestManager.transformRequest(this.url.replace("{fontstack}", fontstack).replace("{range}", `${begin}-${end}`), "Glyphs");
1612
+ const response = await getArrayBuffer(request, new AbortController());
1613
+ if (!response?.data) throw new Error(`Could not load glyph range. range: ${range}, ${begin}-${end}`);
1614
+ const glyphs = {};
1615
+ for (const glyph of parseGlyphPbf(response.data)) glyphs[glyph.id] = glyph;
1616
+ return glyphs;
1617
+ }
1390
1618
  _warnOnMissingGlyphRange(glyph, range, id, err) {
1391
1619
  const begin = range * 256;
1392
1620
  const end = begin + 255;
@@ -1400,13 +1628,13 @@ var GlyphManager = class GlyphManager {
1400
1628
  return !!this.localIdeographFontFamily && codePointUsesLocalIdeographFontFamily(id);
1401
1629
  }
1402
1630
  /**
1403
- * Draws a glyph offscreen using TinySDF, creating a TinySDF instance lazily.
1631
+ * Draws a glyph offscreen using TinySDF, created lazily. The whole cluster goes to TinySDF, which
1632
+ * is what lets the browser's text engine place a letter's marks on it.
1633
+ *
1634
+ * @param fontFaceFamily - the CSS family of the `font-faces` file covering this codepoint, if any
1404
1635
  */
1405
- async _drawGlyph(entry, stack, id) {
1406
- const usesLocalIdeographFontFamily = stack === defaultStack && this.localIdeographFontFamily !== "" && this._charUsesLocalIdeographFontFamily(id);
1407
- const tinySDFKey = usesLocalIdeographFontFamily ? "ideographTinySDF" : "tinySDF";
1408
- entry[tinySDFKey] ||= this._createTinySDF(usesLocalIdeographFontFamily ? this.localIdeographFontFamily : stack);
1409
- const char = (await entry[tinySDFKey]).draw(String.fromCodePoint(id));
1636
+ async _drawGlyph(entry, stack, id, fontFaceFamily) {
1637
+ const char = (await this._getTinySDF(entry, stack, id, fontFaceFamily)).draw(id);
1410
1638
  /**
1411
1639
  * TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph.
1412
1640
  * Server-generated fonts specify "top" relative to an origin above the em box (the origin
@@ -1422,9 +1650,9 @@ var GlyphManager = class GlyphManager {
1422
1650
  */
1423
1651
  const topAdjustment = 27.5;
1424
1652
  const leftAdjustment = .5;
1425
- const isControl = /^\p{gc=Cf}+$/u.test(String.fromCodePoint(id));
1653
+ const isControl = /^\p{gc=Cf}+$/u.test(id);
1426
1654
  return {
1427
- id,
1655
+ id: id.codePointAt(0),
1428
1656
  bitmap: new AlphaImage({
1429
1657
  width: char.width || 60,
1430
1658
  height: char.height || 60
@@ -1439,28 +1667,59 @@ var GlyphManager = class GlyphManager {
1439
1667
  }
1440
1668
  };
1441
1669
  }
1442
- async _createTinySDF(stack) {
1670
+ /**
1671
+ * Returns the TinySDF that draws this grapheme, created lazily. A stack keeps one per font
1672
+ * selection, so a fallback cannot bleed into the rest of the text, and one more per font file
1673
+ * for the clusters drawn from it, which need a wider canvas.
1674
+ *
1675
+ * A font file carries its own weight and style, so neither is sniffed out of the family name.
1676
+ * Where no file covers the grapheme, `localIdeographFontFamily` beats the last resort fontstack.
1677
+ */
1678
+ _getTinySDF(entry, stack, id, fontFaceFamily) {
1679
+ if (fontFaceFamily) {
1680
+ const cluster = isCluster(id);
1681
+ const cache = cluster ? "clusterTinySDFs" : "fontFaceTinySDFs";
1682
+ entry[cache] ??= {};
1683
+ entry[cache][fontFaceFamily] ||= this._createTinySDF(fontFaceFamily, false, cluster ? clusterEmsWide : 1);
1684
+ return entry[cache][fontFaceFamily];
1685
+ }
1686
+ const usesLocalIdeographFontFamily = stack === defaultStack && this.localIdeographFontFamily !== "" && this._charUsesLocalIdeographFontFamily(id.codePointAt(0));
1687
+ const cache = usesLocalIdeographFontFamily ? "ideographTinySDF" : "tinySDF";
1688
+ entry[cache] ||= this._createTinySDF(usesLocalIdeographFontFamily ? this.localIdeographFontFamily : stack);
1689
+ return entry[cache];
1690
+ }
1691
+ /**
1692
+ * Builds the TinySDF that draws with a given font selection.
1693
+ *
1694
+ * TinySDF derives its canvas from `fontSize + buffer * 4`, so the buffer is the only way in to a
1695
+ * wider one. It also stands for the padding the atlas expects to be `GLYPH_PBF_BORDER`, so the
1696
+ * two are passed separately.
1697
+ *
1698
+ * @param emsWide - how wide the glyphs may be, in font sizes, before TinySDF cuts them off
1699
+ */
1700
+ async _createTinySDF(stack, sniffFontStyles = true, emsWide = 1) {
1443
1701
  const fontFamilies = stack ? stack.split(",") : [];
1444
1702
  fontFamilies.push(defaultGenericFontFamily);
1445
1703
  const fontFamily = fontFamilies.map((fontName) => /[-\w]+/.test(fontName) ? fontName : `'${CSS.escape(fontName)}'`).join(",");
1446
1704
  const fontSize = 48;
1447
- const fontWeight = this._fontWeight(fontFamilies[0]);
1448
- const fontStyle = this._fontStyle(fontFamilies[0]);
1705
+ const fontWeight = sniffFontStyles ? this._fontWeight(fontFamilies[0]) : void 0;
1706
+ const fontStyle = sniffFontStyles ? this._fontStyle(fontFamilies[0]) : "normal";
1449
1707
  if (typeof document !== "undefined" && document.fonts?.load) try {
1450
1708
  await document.fonts.load(`${fontStyle} ${fontWeight || "normal"} ${fontSize}px ${fontFamily}`);
1451
1709
  } catch (e) {
1452
1710
  warnOnce(`Failed to load font "${fontFamily}": ${ensureError(e).message}`);
1453
1711
  }
1454
- return new GlyphManager.TinySDF({
1712
+ const padding = 6;
1713
+ return this.createRasterizer({
1455
1714
  fontSize,
1456
- buffer: 6,
1715
+ buffer: Math.max(padding, Math.ceil(fontSize * (emsWide - 1) / 4)),
1457
1716
  radius: 16,
1458
1717
  cutoff: .25,
1459
1718
  fontFamily,
1460
1719
  fontWeight,
1461
1720
  fontStyle,
1462
1721
  lang: this.lang
1463
- });
1722
+ }, padding);
1464
1723
  }
1465
1724
  /**
1466
1725
  * Sniffs the font style out of a font family name.
@@ -1502,11 +1761,13 @@ var GlyphManager = class GlyphManager {
1502
1761
  const entry = this.entries[stack];
1503
1762
  entry.tinySDF = null;
1504
1763
  entry.ideographTinySDF = null;
1764
+ entry.fontFaceTinySDFs = {};
1505
1765
  entry.glyphs = {};
1506
1766
  entry.requests = {};
1507
1767
  entry.ranges = {};
1508
1768
  }
1509
1769
  this.entries = {};
1770
+ this.fontFaceManager.destroy();
1510
1771
  }
1511
1772
  };
1512
1773
  //#endregion
@@ -4063,6 +4324,18 @@ const BILINEAR_FORESHORTENING = 512;
4063
4324
  * const bitmap = await createImageBitmap(myCanvas);
4064
4325
  * mySource.updateImage({image: bitmap});
4065
4326
  *
4327
+ * // create an empty source (no url), then feed it entirely via updateImage
4328
+ * map.addSource('empty id', {
4329
+ * type: 'image',
4330
+ * coordinates: [
4331
+ * [-76.54, 39.18],
4332
+ * [-76.52, 39.18],
4333
+ * [-76.52, 39.17],
4334
+ * [-76.54, 39.17]
4335
+ * ]
4336
+ * });
4337
+ * (map.getSource('empty id') as ImageSource).updateImage({image: bitmap});
4338
+ *
4066
4339
  * map.removeSource('some id'); // remove
4067
4340
  * ```
4068
4341
  */
@@ -4089,6 +4362,11 @@ var ImageSource = class extends Evented {
4089
4362
  this.options = options;
4090
4363
  }
4091
4364
  async load(newCoordinates) {
4365
+ if (this.options.url === void 0) {
4366
+ this._loaded = true;
4367
+ this._finishLoading();
4368
+ return;
4369
+ }
4092
4370
  this._loaded = false;
4093
4371
  this.fire(new MapSourceDataEvent("dataloading"));
4094
4372
  this.url = this.options.url;
@@ -4271,11 +4549,12 @@ var ImageSource = class extends Evented {
4271
4549
  } else tile.state = "errored";
4272
4550
  }
4273
4551
  serialize() {
4274
- return {
4552
+ const spec = {
4275
4553
  type: "image",
4276
- url: this.options.url,
4277
4554
  coordinates: this.coordinates
4278
4555
  };
4556
+ if (this.options.url !== void 0) spec.url = this.options.url;
4557
+ return spec;
4279
4558
  }
4280
4559
  hasTransition() {
4281
4560
  return false;
@@ -9968,11 +10247,13 @@ var TerrainTileManager = class extends Evented {
9968
10247
  * Load Terrain Tiles, create internal render-to-texture tiles, free GPU memory.
9969
10248
  * @param transform - the operation to do
9970
10249
  * @param terrain - the terrain
10250
+ * @returns true when the set of renderable tiles changed
9971
10251
  */
9972
10252
  update(transform, terrain) {
9973
10253
  this.tileManager.update(transform, terrain);
9974
10254
  this._renderableTilesKeys = [];
9975
10255
  const keys = {};
10256
+ let changed = false;
9976
10257
  for (const tileID of coveringTiles(transform, {
9977
10258
  tileSize: this.tileSize,
9978
10259
  minzoom: this.minzoom,
@@ -9988,12 +10269,15 @@ var TerrainTileManager = class extends Evented {
9988
10269
  ortho(tileID.terrainRttPosMatrix32f, 0, EXTENT, EXTENT, 0, 0, 1);
9989
10270
  this._tiles[tileID.key] = new Tile(tileID, this.tileSize);
9990
10271
  this._lastTilesetChange = now();
10272
+ changed = true;
9991
10273
  }
9992
10274
  }
9993
10275
  for (const key in this._tiles) if (!keys[key]) {
9994
10276
  this._tiles[key].releaseRTT(this.tileManager.map.painter);
9995
10277
  delete this._tiles[key];
10278
+ changed = true;
9996
10279
  }
10280
+ return changed;
9997
10281
  }
9998
10282
  /**
9999
10283
  * Release the RTT objects for `tileID` (and its ancestors/descendants),
@@ -10280,11 +10564,19 @@ var Terrain = class {
10280
10564
  }
10281
10565
  /**
10282
10566
  * Get the elevation for given {@link LngLat} in respect of exaggeration.
10283
- * This will traverse up the zoom levels to find the first tile with data to return.
10567
+ * Where the location is covered by a rendered tile with loaded DEM data this samples the
10568
+ * rendered surface, so the result agrees with what is drawn; elsewhere it traverses up the
10569
+ * zoom levels to find the first tile with data to return.
10284
10570
  * @param lnglat - the location
10285
10571
  * @returns the elevation
10286
10572
  */
10287
10573
  getElevationForLngLat(lnglat, transform) {
10574
+ const index = this.getCoverageIndex();
10575
+ if (index) {
10576
+ const mercator = MercatorCoordinate.fromLngLat(lnglat);
10577
+ const sample = sampleAt(index, this.exaggeration, mercator.x, mercator.y);
10578
+ if (sample.demLoaded) return sample.elevation;
10579
+ }
10288
10580
  const terrainCoveringTiles = coveringTiles(transform, {
10289
10581
  maxzoom: this.tileManager.maxzoom,
10290
10582
  minzoom: this.tileManager.minzoom,
@@ -10591,6 +10883,7 @@ var Terrain = class {
10591
10883
  };
10592
10884
  const NOT_COVERED = {
10593
10885
  covered: false,
10886
+ demLoaded: false,
10594
10887
  elevation: 0
10595
10888
  };
10596
10889
  /**
@@ -10612,10 +10905,12 @@ function sampleAt(index, exaggeration, mercatorX, mercatorY) {
10612
10905
  const sampler = index.samplerPerTile.get(key);
10613
10906
  if (!sampler) return {
10614
10907
  covered: true,
10908
+ demLoaded: false,
10615
10909
  elevation: 0
10616
10910
  };
10617
10911
  return {
10618
10912
  covered: true,
10913
+ demLoaded: true,
10619
10914
  elevation: sampler(Math.min((scaledX - tileX) * EXTENT, MAX_TILE_COORD), Math.min((scaledY - tileY) * EXTENT, MAX_TILE_COORD), EXTENT) * exaggeration
10620
10915
  };
10621
10916
  }
@@ -10960,7 +11255,7 @@ var MercatorTransform = class MercatorTransform {
10960
11255
  }
10961
11256
  recalculateZoomAndCenter(terrain) {
10962
11257
  const center = this.screenPointToLocation(this.centerPoint, terrain);
10963
- const elevation = terrain ? terrain.getElevationForLngLatZoom(center, this._helper._tileZoom) : 0;
11258
+ const elevation = terrain ? terrain.getElevationForLngLat(center, this) : 0;
10964
11259
  this._helper.recalculateZoomAndCenter(elevation);
10965
11260
  }
10966
11261
  /**
@@ -13301,7 +13596,7 @@ function globeSampleAt(ray, t) {
13301
13596
  const elevation = Math.abs(lngLat.lat) > 85.051129 ? 0 : sample.elevation;
13302
13597
  return {
13303
13598
  sample: {
13304
- covered: sample.covered,
13599
+ ...sample,
13305
13600
  elevation
13306
13601
  },
13307
13602
  radius,
@@ -14181,6 +14476,8 @@ var Style = class extends Evented {
14181
14476
  this._glyphsDidChange = false;
14182
14477
  this._updatedPaintProps = {};
14183
14478
  this._layerOrderChanged = false;
14479
+ this._symbolPlacementTriggered = false;
14480
+ this._placedProjectionTransition = void 0;
14184
14481
  this.crossTileSymbolIndex = new ((this.crossTileSymbolIndex?.constructor) || Object)();
14185
14482
  this.pauseablePlacement = void 0;
14186
14483
  this.placement = void 0;
@@ -14275,6 +14572,7 @@ var Style = class extends Evented {
14275
14572
  if (nextState.sprite) this._loadSprite(nextState.sprite);
14276
14573
  else this.imageManager.setLoaded(true);
14277
14574
  this.glyphManager.setURL(nextState.glyphs);
14575
+ this.glyphManager.setFontFaces(nextState["font-faces"]);
14278
14576
  this._createLayers();
14279
14577
  this.light = new Light(this.stylesheet.light ?? {}, this._globalState);
14280
14578
  this._setProjectionInternal(this.stylesheet.projection?.type || "mercator");
@@ -14538,6 +14836,9 @@ var Style = class extends Evented {
14538
14836
  case "setGlyphs":
14539
14837
  operations.push(() => this.setGlyphs.apply(this, op.args));
14540
14838
  break;
14839
+ case "setFontFaces":
14840
+ operations.push(() => this.setFontFaces.apply(this, op.args));
14841
+ break;
14541
14842
  case "setSprite":
14542
14843
  operations.push(() => this.setSprite.apply(this, op.args));
14543
14844
  break;
@@ -14863,6 +15164,7 @@ var Style = class extends Evented {
14863
15164
  if (isRasterStyleLayer(layer) && name === "raster-fade-duration") this.tileManagers[layer.source].setRasterFadeDuration(value);
14864
15165
  this._changed = true;
14865
15166
  this._updatedPaintProps[layer.id] = true;
15167
+ if (layer.type === "symbol") this.triggerSymbolPlacement();
14866
15168
  this._serializedLayers = null;
14867
15169
  }
14868
15170
  getPaintProperty(layer, name) {
@@ -14961,8 +15263,10 @@ var Style = class extends Evented {
14961
15263
  pitch: myStyleSheet.pitch,
14962
15264
  sprite: myStyleSheet.sprite,
14963
15265
  glyphs: myStyleSheet.glyphs,
15266
+ "font-faces": myStyleSheet["font-faces"],
14964
15267
  transition: myStyleSheet.transition,
14965
15268
  projection: myStyleSheet.projection,
15269
+ state: myStyleSheet.state,
14966
15270
  sources,
14967
15271
  layers,
14968
15272
  terrain
@@ -15120,6 +15424,7 @@ var Style = class extends Evented {
15120
15424
  for (const key in this.tileManagers) this.tileManagers[key].reload();
15121
15425
  }
15122
15426
  _validate(validate, key, value, props, options = {}) {
15427
+ if (options.validate === false) return false;
15123
15428
  return validateAndEmit(this, validate, {
15124
15429
  key,
15125
15430
  style: this.serialize(),
@@ -15165,6 +15470,26 @@ var Style = class extends Evented {
15165
15470
  _generateCollisionBoxes() {
15166
15471
  for (const id in this.tileManagers) this._reloadSource(id);
15167
15472
  }
15473
+ /**
15474
+ * Re-places symbols on the next frame.
15475
+ * Placement is skipped while its inputs look unchanged, so call this when something
15476
+ * the map cannot see for itself has moved symbols.
15477
+ */
15478
+ triggerSymbolPlacement() {
15479
+ this._symbolPlacementTriggered = true;
15480
+ }
15481
+ /**
15482
+ * Whether re-running symbol placement could produce a different result than the last run.
15483
+ * Every camera and viewport parameter placement can observe perturbs the model-view-projection
15484
+ * matrix, so comparing the matrix covers them all. Inputs the matrix cannot see arrive as
15485
+ * {@link triggerSymbolPlacement} calls.
15486
+ *
15487
+ * @internal
15488
+ */
15489
+ _placementInputsChanged(transform, showCollisionBoxes, crossSourceCollisions) {
15490
+ const lastPlacement = this.pauseablePlacement;
15491
+ return !lastPlacement || this._symbolPlacementTriggered || this._placedProjectionTransition !== this.projection?.transitionState || lastPlacement._showCollisionBoxes !== showCollisionBoxes || lastPlacement.placement.collisionGroups.crossSourceCollisions !== crossSourceCollisions || lastPlacement.placement.transform.renderWorldCopies !== transform.renderWorldCopies || !exactEquals(lastPlacement.placement.transform.modelViewProjectionMatrix, transform.modelViewProjectionMatrix);
15492
+ }
15168
15493
  _updatePlacement(transform, showCollisionBoxes, fadeDuration, crossSourceCollisions, forceFullPlacement = false) {
15169
15494
  let symbolBucketsChanged = false;
15170
15495
  let placementCommitted = false;
@@ -15181,12 +15506,17 @@ var Style = class extends Evented {
15181
15506
  }
15182
15507
  this.crossTileSymbolIndex.pruneUnusedLayers(this._order);
15183
15508
  forceFullPlacement ||= this._layerOrderChanged || fadeDuration === 0;
15184
- if (forceFullPlacement || !this.pauseablePlacement || this.pauseablePlacement.isDone() && !this.placement.stillRecent(now(), transform.zoom)) {
15509
+ const placementInputsChanged = symbolBucketsChanged || this._placementInputsChanged(transform, showCollisionBoxes, crossSourceCollisions);
15510
+ const placementSettled = this.pauseablePlacement?.isDone() && !this.placement.stillRecent(now(), transform.zoom);
15511
+ if (forceFullPlacement || !this.pauseablePlacement || placementSettled && (placementInputsChanged || this.placement.stale)) {
15512
+ this._symbolPlacementTriggered = false;
15513
+ this._placedProjectionTransition = this.projection?.transitionState;
15185
15514
  this.pauseablePlacement = new PauseablePlacement(transform, this.map.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
15186
15515
  this._layerOrderChanged = false;
15187
15516
  }
15188
- if (this.pauseablePlacement.isDone()) this.placement.setStale();
15189
- else {
15517
+ if (this.pauseablePlacement.isDone()) {
15518
+ if (placementInputsChanged) this.placement.setStale();
15519
+ } else {
15190
15520
  this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles);
15191
15521
  if (this.pauseablePlacement.isDone()) {
15192
15522
  this.placement = this.pauseablePlacement.commit(now());
@@ -15223,11 +15553,22 @@ var Style = class extends Evented {
15223
15553
  setGlyphs(glyphsUrl, options = {}) {
15224
15554
  this._checkLoaded();
15225
15555
  if (glyphsUrl && this._validate(validateStyle.glyphs, "glyphs", glyphsUrl, null, options)) return;
15556
+ this._changed = true;
15226
15557
  this._glyphsDidChange = true;
15227
15558
  this.stylesheet.glyphs = glyphsUrl;
15228
15559
  this.glyphManager.entries = {};
15229
15560
  this.glyphManager.setURL(glyphsUrl);
15230
15561
  }
15562
+ getFontFaces() {
15563
+ return this.stylesheet["font-faces"] || null;
15564
+ }
15565
+ setFontFaces(fontFaces) {
15566
+ this._checkLoaded();
15567
+ this._changed = true;
15568
+ this._glyphsDidChange = true;
15569
+ this.stylesheet["font-faces"] = fontFaces;
15570
+ this.glyphManager.setFontFaces(fontFaces);
15571
+ }
15231
15572
  async getDashes(mapId, params) {
15232
15573
  const result = {};
15233
15574
  for (const [key, dash] of Object.entries(params.dashes)) result[key] = this.lineAtlas.getDash(dash.dasharray, dash.round);
@@ -17271,7 +17612,7 @@ function drawSymbols(painter, tileManager, layer, coords, variableOffsets, rende
17271
17612
  const stencilMode = StencilMode.disabled;
17272
17613
  const colorMode = painter.colorModeForRenderPass();
17273
17614
  if (layer._unevaluatedLayout.hasValue("text-variable-anchor") || layer._unevaluatedLayout.hasValue("text-variable-anchor-offset")) updateVariableAnchors(coords, painter, layer, tileManager, layer.layout.get("text-rotation-alignment"), layer.layout.get("text-pitch-alignment"), layer.paint.get("text-translate"), layer.paint.get("text-translate-anchor"), variableOffsets);
17274
- if (layer.paint.get("icon-opacity").constantOr(1) !== 0) drawLayerSymbols(painter, tileManager, layer, coords, false, layer.paint.get("icon-translate"), layer.paint.get("icon-translate-anchor"), layer.layout.get("icon-rotation-alignment"), layer.layout.get("icon-pitch-alignment"), layer.layout.get("icon-keep-upright"), stencilMode, colorMode, isRenderingToTexture);
17615
+ if (layer.paint.get("icon-opacity").constantOr(1) !== 0) drawLayerSymbols(painter, tileManager, layer, coords, false, layer.paint.get("icon-translate"), layer.paint.get("icon-translate-anchor"), layer.layout.get("icon-rotation-alignment").constantOr("viewport"), layer.layout.get("icon-pitch-alignment"), layer.layout.get("icon-keep-upright"), stencilMode, colorMode, isRenderingToTexture);
17275
17616
  if (layer.paint.get("text-opacity").constantOr(1) !== 0) drawLayerSymbols(painter, tileManager, layer, coords, true, layer.paint.get("text-translate"), layer.paint.get("text-translate-anchor"), layer.layout.get("text-rotation-alignment"), layer.layout.get("text-pitch-alignment"), layer.layout.get("text-keep-upright"), stencilMode, colorMode, isRenderingToTexture);
17276
17617
  if (tileManager.map.showCollisionBoxes) {
17277
17618
  drawCollisionDebug(painter, tileManager, layer, coords, true);
@@ -19382,7 +19723,8 @@ var Painter = class Painter {
19382
19723
  //#endregion
19383
19724
  //#region src/util/gpu_initialization_error.ts
19384
19725
  /**
19385
- * Thrown (via the map's `error` event) when a GPU rendering context cannot be created.
19726
+ * Thrown by the `Map` constructor when a GPU rendering context cannot be created,
19727
+ * or fired via the map's `error` event when recreating the context after a `webglcontextrestored` fails.
19386
19728
  *
19387
19729
  * Carries the canvas attributes that were requested and, when the browser provided one,
19388
19730
  * the originating `webglcontextcreationerror` `statusMessage`.
@@ -22261,7 +22603,7 @@ var Camera = class extends Evented {
22261
22603
  let bearingChanged = false, pitchChanged = false;
22262
22604
  let rollChanged = false;
22263
22605
  const oldZoom = tr.zoom;
22264
- if (this.terrain) tr.setElevation(this.terrain.getElevationForLngLatZoom(options.center ? LngLat.convert(options.center) : tr.center, options.zoom || tr.tileZoom));
22606
+ if (this.terrain) tr.setElevation(this.terrain.getElevationForLngLat(options.center ? LngLat.convert(options.center) : tr.center, tr));
22265
22607
  this.cameraHelper.handleJumpToCenterZoom(tr, options);
22266
22608
  const zoomChanged = tr.zoom !== oldZoom;
22267
22609
  if ("elevation" in options && tr.elevation !== +options.elevation) tr.setElevation(+options.elevation);
@@ -22384,13 +22726,13 @@ var Camera = class extends Evented {
22384
22726
  _prepareElevation(center) {
22385
22727
  this._elevationCenter = center;
22386
22728
  this._elevationStart = this.transform.elevation;
22387
- this._elevationTarget = this.terrain.getElevationForLngLatZoom(center, this.transform.tileZoom);
22729
+ this._elevationTarget = this.terrain.getElevationForLngLat(center, this.transform);
22388
22730
  this.elevationFreeze = true;
22389
22731
  }
22390
22732
  _updateElevation(k) {
22391
22733
  if (this._elevationStart === void 0 || this._elevationCenter === void 0) this._prepareElevation(this.transform.center);
22392
22734
  this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter, this.transform.tileZoom));
22393
- const elevation = this.terrain.getElevationForLngLatZoom(this._elevationCenter, this.transform.tileZoom);
22735
+ const elevation = this.terrain.getElevationForLngLat(this._elevationCenter, this.transform);
22394
22736
  if (k < 1 && elevation !== this._elevationTarget) {
22395
22737
  const pitch1 = this._elevationTarget - this._elevationStart;
22396
22738
  const pitch2 = (elevation - (pitch1 * k + this._elevationStart)) / (1 - k);
@@ -23200,8 +23542,12 @@ var Map$1 = class extends Evented {
23200
23542
  style: null,
23201
23543
  images: null
23202
23544
  };
23203
- this._setupPainter();
23204
- if (!this.painter) return;
23545
+ try {
23546
+ this._setupPainter();
23547
+ } catch (error) {
23548
+ this.fire(new ErrorEvent(error));
23549
+ return;
23550
+ }
23205
23551
  this.resize();
23206
23552
  this._update();
23207
23553
  this._resizeInternal();
@@ -23266,13 +23612,17 @@ var Map$1 = class extends Evented {
23266
23612
  this.cancelPendingTileRequestsWhileZooming = resolvedOptions.cancelPendingTileRequestsWhileZooming === true;
23267
23613
  this.setAnisotropicFilterPitch(resolvedOptions.anisotropicFilterPitch);
23268
23614
  if (resolvedOptions.reduceMotion !== void 0) browser.prefersReducedMotion = resolvedOptions.reduceMotion;
23269
- this._imageQueueHandle = ImageRequest.addThrottleControl(() => this.isMoving());
23270
23615
  this._requestManager = new RequestManager(resolvedOptions.transformRequest);
23271
23616
  this._container = this._resolveContainer(resolvedOptions.container);
23272
23617
  if (resolvedOptions.maxBounds) this.setMaxBounds(resolvedOptions.maxBounds);
23273
23618
  this._setupContainer();
23274
- this._setupPainter();
23275
- if (!this.painter) return;
23619
+ try {
23620
+ this._setupPainter();
23621
+ } catch (error) {
23622
+ this._cleanupContainer();
23623
+ throw error;
23624
+ }
23625
+ this._imageQueueHandle = ImageRequest.addThrottleControl(() => this.isMoving());
23276
23626
  this.on("move", () => this._update(false));
23277
23627
  this.on("moveend", () => this._update(false));
23278
23628
  this.on("zoom", () => this._update(true));
@@ -24247,14 +24597,17 @@ var Map$1 = class extends Evented {
24247
24597
  /**
24248
24598
  * Returns the map's minimum allowable zoom level.
24249
24599
  *
24600
+ * @param constrained - If `true`, returns the effective minimum zoom after applying the map's viewport constraints.
24601
+ * If `false` or omitted, returns the configured minimum zoom.
24250
24602
  * @returns minZoom
24251
24603
  * @example
24252
24604
  * ```ts
24253
24605
  * let minZoom = map.getMinZoom();
24254
24606
  * ```
24255
24607
  */
24256
- getMinZoom() {
24257
- return this._camera.transform.minZoom;
24608
+ getMinZoom(constrained = false) {
24609
+ const transform = this._camera.transform;
24610
+ return constrained ? transform.applyConstrain(transform.center, transform.minZoom).zoom : transform.minZoom;
24258
24611
  }
24259
24612
  /**
24260
24613
  * Sets or clears the map's maximum zoom level.
@@ -25049,10 +25402,11 @@ var Map$1 = class extends Evented {
25049
25402
  this.painter.renderToTexture = new RenderToTexture(this.painter, this.terrain);
25050
25403
  this._camera.terrain = this.terrain;
25051
25404
  this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
25052
- this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
25405
+ this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform));
25053
25406
  this._terrainDataCallback = (e) => this._handleTerrainDataEvent(e, options.source);
25054
25407
  this.style.on("data", this._terrainDataCallback);
25055
25408
  }
25409
+ this.style.triggerSymbolPlacement();
25056
25410
  this.fire(new MapTerrainEvent({ terrain: options }));
25057
25411
  return this;
25058
25412
  }
@@ -25062,10 +25416,13 @@ var Map$1 = class extends Evented {
25062
25416
  return;
25063
25417
  }
25064
25418
  const isTerrainSourceEvent = event.sourceId === terrainSourceId;
25065
- if (isTerrainSourceEvent) this.terrain.resetElevationCache();
25419
+ if (isTerrainSourceEvent) {
25420
+ this.terrain.resetElevationCache();
25421
+ this.style.triggerSymbolPlacement();
25422
+ }
25066
25423
  if (isTerrainSourceEvent && event.tile && !this._camera.elevationFreeze) {
25067
25424
  this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
25068
- if (this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
25425
+ if (this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform));
25069
25426
  }
25070
25427
  if (!event.tile) return;
25071
25428
  if (event.source?.type === "image") {
@@ -25720,6 +26077,42 @@ var Map$1 = class extends Evented {
25720
26077
  return this.style.getGlyphsUrl();
25721
26078
  }
25722
26079
  /**
26080
+ * Sets the value of the style's `font-faces` property, which points at the font files used to
26081
+ * draw text that the style's `glyphs` URL does not cover. Pass a falsy value (null or undefined)
26082
+ * to unset it.
26083
+ *
26084
+ * The files are handed to the browser's CSS Font Loading API, so any format the browser can
26085
+ * render text with may be used, and requests for them go through `transformRequest` as glyph
26086
+ * requests do. Text is drawn a grapheme cluster at a time, so a letter and the marks written on
26087
+ * it are handed to the browser's text engine together and come back as the one shape they are
26088
+ * written as -- which is what a `glyphs` URL, serving one codepoint at a time, cannot do.
26089
+ *
26090
+ * @param fontFaces - The font faces to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/root/#font-faces).
26091
+ * A declaration this cannot make sense of is skipped with a warning, as is a font file that
26092
+ * fails to load, so the text it would have drawn falls back to the `glyphs` URL.
26093
+ * @example
26094
+ * ```ts
26095
+ * map.setFontFaces({
26096
+ * 'Noto Sans Regular': [
26097
+ * {url: 'https://example.com/NotoSansKhmer-Regular.ttf', 'unicode-range': ['U+1780-17FF']}
26098
+ * ]
26099
+ * });
26100
+ * ```
26101
+ */
26102
+ setFontFaces(fontFaces) {
26103
+ this._lazyInitEmptyStyle();
26104
+ this.style.setFontFaces(fontFaces);
26105
+ return this._update(true);
26106
+ }
26107
+ /**
26108
+ * Returns the value of the style's `font-faces` property.
26109
+ *
26110
+ * @returns The style's font faces, or `null` if it declares none.
26111
+ */
26112
+ getFontFaces() {
26113
+ return this.style.getFontFaces();
26114
+ }
26115
+ /**
25723
26116
  * Adds a sprite to the map's style. Fires the `style` event.
25724
26117
  *
25725
26118
  * @param id - The ID of the sprite to add. Must not conflict with existing sprites.
@@ -26055,12 +26448,28 @@ var Map$1 = class extends Evented {
26055
26448
  ]) positions[positionName] = DOM.create("div", `maplibregl-ctrl-${positionName} `, controlContainer);
26056
26449
  this._container.addEventListener("scroll", this._onMapScroll, false);
26057
26450
  }
26451
+ /**
26452
+ * @internal
26453
+ * Reverses the DOM mutations of {@link Map._setupContainer}, returning the container element to its pre-construction state.
26454
+ */
26455
+ _cleanupContainer() {
26456
+ this._canvas.removeEventListener("webglcontextrestored", this._contextRestored, false);
26457
+ this._canvas.removeEventListener("webglcontextlost", this._contextLost, false);
26458
+ this._canvasContainer.remove();
26459
+ this._controlContainer.remove();
26460
+ this._container.removeEventListener("scroll", this._onMapScroll, false);
26461
+ this._container.classList.remove("maplibregl-map");
26462
+ }
26058
26463
  _resizeCanvas(width, height, pixelRatio) {
26059
26464
  this._canvas.width = Math.floor(pixelRatio * width);
26060
26465
  this._canvas.height = Math.floor(pixelRatio * height);
26061
26466
  this._canvas.style.width = `${width}px`;
26062
26467
  this._canvas.style.height = `${height}px`;
26063
26468
  }
26469
+ /**
26470
+ * @internal
26471
+ * Creates the WebGL2 context and the painter. Throws {@link GPUInitializationError} when the context cannot be created.
26472
+ */
26064
26473
  _setupPainter() {
26065
26474
  const attributes = {
26066
26475
  ...this._canvasContextAttributes,
@@ -26074,10 +26483,7 @@ var Map$1 = class extends Evented {
26074
26483
  creationEvent = event;
26075
26484
  }, { once: true });
26076
26485
  const gl = this._canvas.getContext("webgl2", attributes);
26077
- if (!gl) {
26078
- this.fire(new ErrorEvent(new GPUInitializationError(attributes, creationEvent)));
26079
- return;
26080
- }
26486
+ if (!gl) throw new GPUInitializationError(attributes, creationEvent);
26081
26487
  this.painter = new Painter(gl, this._camera.transform);
26082
26488
  }
26083
26489
  /**
@@ -26175,10 +26581,9 @@ var Map$1 = class extends Evented {
26175
26581
  this.style._updateSources(this._camera.transform);
26176
26582
  }
26177
26583
  if (this.terrain) {
26178
- this.terrain.tileManager.update(this._camera.transform, this.terrain);
26179
- this.terrain.resetElevationCache();
26584
+ if (this.terrain.tileManager.update(this._camera.transform, this.terrain)) this.terrain.resetElevationCache();
26180
26585
  this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
26181
- if (!this._camera.elevationFreeze && this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
26586
+ if (!this._camera.elevationFreeze && this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform));
26182
26587
  } else {
26183
26588
  this._camera.transform.setMinElevationForCurrentTile(0);
26184
26589
  if (this.getCenterClampedToGround()) this._camera.transform.setElevation(0);
@@ -26251,12 +26656,7 @@ var Map$1 = class extends Evented {
26251
26656
  this._resizeObserver?.disconnect();
26252
26657
  const extension = this.painter.context.gl.getExtension("WEBGL_lose_context");
26253
26658
  if (extension?.loseContext) extension.loseContext();
26254
- this._canvas.removeEventListener("webglcontextrestored", this._contextRestored, false);
26255
- this._canvas.removeEventListener("webglcontextlost", this._contextLost, false);
26256
- this._canvasContainer.remove();
26257
- this._controlContainer.remove();
26258
- this._container.removeEventListener("scroll", this._onMapScroll, false);
26259
- this._container.classList.remove("maplibregl-map");
26659
+ this._cleanupContainer();
26260
26660
  this._removed = true;
26261
26661
  this.fire(new MapLibreEvent("remove"));
26262
26662
  }
@@ -26433,7 +26833,7 @@ var NavigationControl = class {
26433
26833
  this._updateZoomButtons = () => {
26434
26834
  const zoom = this._map.getZoom();
26435
26835
  const isMax = zoom === this._map.getMaxZoom();
26436
- const isMin = zoom === this._map.getMinZoom();
26836
+ const isMin = zoom === this._map.getMinZoom(true);
26437
26837
  this._zoomInButton.disabled = isMax;
26438
26838
  this._zoomOutButton.disabled = isMin;
26439
26839
  this._zoomInButton.setAttribute("aria-disabled", isMax.toString());
@@ -26487,7 +26887,7 @@ var NavigationControl = class {
26487
26887
  if (this.options.showZoom) {
26488
26888
  this._setButtonTitle(this._zoomInButton, "ZoomIn");
26489
26889
  this._setButtonTitle(this._zoomOutButton, "ZoomOut");
26490
- this._map.on("zoom", this._updateZoomButtons);
26890
+ this._map.on("move", this._updateZoomButtons);
26491
26891
  this._updateZoomButtons();
26492
26892
  }
26493
26893
  if (this.options.showCompass) {
@@ -26503,7 +26903,7 @@ var NavigationControl = class {
26503
26903
  /** {@inheritDoc IControl.onRemove} */
26504
26904
  onRemove() {
26505
26905
  this._container.remove();
26506
- if (this.options.showZoom) this._map.off("zoom", this._updateZoomButtons);
26906
+ if (this.options.showZoom) this._map.off("move", this._updateZoomButtons);
26507
26907
  if (this.options.showCompass) {
26508
26908
  if (this.options.visualizePitch) this._map.off("pitch", this._rotateCompassArrow);
26509
26909
  if (this.options.visualizeRoll) this._map.off("roll", this._rotateCompassArrow);