maplibre-gl 6.11.0 → 6.11.1

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.
package/README.md CHANGED
@@ -104,7 +104,7 @@ Silver:
104
104
 
105
105
  <a href="https://www.caltopo.com/"><img src="https://maplibre.org/img/caltopo-logo.svg" alt="Logo Caltopo" width="25%"/></a>
106
106
 
107
- <a href="https://www.caltopo.com/"><img src="https://maplibre.org/img/smartmaps-logo.svg" alt="Logo SmartMaps" width="25%"/></a>
107
+ <a href="https://www.smartmaps.net/"><img src="https://maplibre.org/img/smartmaps-logo.svg" alt="Logo SmartMaps" width="25%"/></a>
108
108
 
109
109
  Backers and Supporters:
110
110
 
@@ -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.11.0/LICENSE.txt
3
+ * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.11.1/LICENSE.txt
4
4
  */
5
5
  import { $ as projectToWorldCoordinates, $n as differenceOfAnglesDegrees, $r as add, $t as Transitionable, A as evaluateSizeForZoom, Ai as rotateX, An as addProtocol, Ar as scaleZoom, At as toEvaluationFeature, B as isCluster, Bi as rotate, Bn as arrayBufferToImage, Br as EXTENT, Bt as SegmentVector, C as addDynamicAttributes, Ci as exactEquals, Cn as GLOBAL_DISPATCHER_ID, Cr as pointPlaneSignedDistance, Ct as HEATMAP_FULL_RENDER_FBO_KEY, Di as multiply, Dn as getVideo, Dr as remapSaturate, Dt as RGBAImage, E as TextAnchorEnum, Ei as invert$1, En as getReferrer, Er as readImageUsingVideoFrame, Et as AlphaImage, F as potpack, Fi as create$4, Fn as isAbortError, Fr as uniqueId, Ft as Uniform4f, Gn as createIdentityMat4f32, Gr as zero, Gt as PosArray, Hi as offscreenCanvasSupported, Hn as bezier, Hr as length$1, Ht as CollisionCircleLayoutArray, I as isStyleImageWebGLData, Ii as fromRotation, In as throwIfAborted, Ir as warnOnce, It as UniformColor, J as cameraDirectionFromPitchBearing, Jn as createVec3f64, Jr as multiply$1, Jt as TriangleIndexArray, K as isFillExtrusionStyleLayer, Kn as createIdentityMat4f64, Kr as fromEuler, Kt as QuadTriangleArray, L as renderStyleImage, Li as create$2, Lr as wrap, Lt as UniformColorArray, M as getAnchorAlignment, Mi as rotateZ, Mn as removeProtocol, Mr as subscribe, Mt as Uniform1i, Ni as scale, Nn as config, Nr as threePlaneIntersection, Nt as Uniform2f, O as clipLine, Oi as ortho, On as makeRequest, Or as rollPitchBearingEqual, Ot as isCircleStyleLayer, P as ImagePosition, Pi as translate, Pn as AbortError, Pr as translatePosition, Pt as Uniform3f, Q as maxMercatorHorizonAngle, Qn as degreesToRadians, Qr as transformMat4, Qt as Properties, R as parseGlyphPbf, Ri as determinant, Rn as MAX_VALID_LATITUDE, Rr as zoomScale, Rt as UniformFloatArray, S as SymbolBucket, Si as equals, Sn as AJAXError, Sr as pick, St as isHillshadeStyleLayer, T as getAnchorJustification, Ti as identity, Tn as getJSON, Tr as rayPlaneIntersection, Tt as renderColorRamp, U as collisionCircleLayout, Ui as Point, Un as clamp, Ur as scale$3, Ut as LineStripIndexArray, V as codePointUsesLocalIdeographFontFamily, Vi as isOffscreenCanvasDistorted, Vn as arrayBufferToImageBitmap, Vr as create$3, Vt as CollisionBoxArray, W as isLineStyleLayer, Wn as clone$2, Wr as sqrLen, Wt as Pos3dArray, X as cameraMercatorCoordinateFromCenterAndRotation, Xn as deepEqual, Xr as mul, Xt as isRasterStyleLayer, Y as cameraMercatorCoordinate, Yn as createVec4f64, Yr as slerp, Yt as createLayout, Z as getMercatorHorizon, Zn as defaultEasing, Zr as scale$2, Zt as DataConstantProperty, _ as createStyleLayer, _i as transformQuat, _n as derefLayers, _r as lerp, _t as SubdivisionGranularityExpression, a as GeoJSONFeature, ai as length, an as validateAndEmit, ar as findLineIntersection, at as lngFromMercatorX, b as isBackgroundStyleLayer, bi as copy, bn as Event, br as nextPowerOfTwo, ci as rotateX$1, cn as emptyStyle, cr as getEdgeTiles, ct as mercatorZfromAltitude, d as OverscaledTileID, di as scale$1, dr as isImageBitmap, ei as clone, en as EvaluationParameters, er as distanceOfAnglesRadians, et as tileCoordinatesToMercatorCoordinates, f as UnwrappedTileID, fi as scaleAndAdd, fn as interpolateFactory, fr as isPointableEvent, ft as EXTENT_BOUNDS, g as Actor, gi as transformMat4$1, gn as diff, gr as isWorker, gt as SOUTH_POLE_Y, h as isInBoundsForZoomLngLat, hi as transformMat3, hn as ValidationError, hr as isTouchableOrPointableType, ht as NORTH_POLE_Y, ii as len, in as emitValidationErrors, ir as filterObject, it as latFromMercatorY, ji as rotateY$1, jn as getProtocol, jr as sphericalToCartesian, jt as Uniform1f, k as evaluateSizeForFeature, ki as perspective, kn as sameOrigin, kr as rollPitchBearingToQuat, kt as polygonIntersectsPolygon, li as rotateY, lt as LngLat, m as compareTileId, mi as subtract, mn as Color, mr as isTouchableEvent, mt as isFillStyleLayer, n as TileCache, ni as distance, nr as evaluateZoomSnap, nt as MercatorCoordinate, oi as negate, on as validateStyle, or as getAABB, ot as mercatorXfromLng, p as calculateTileKey, pi as sub, pn as ProjectionDefinition, pr as isSafari, pt as Bounds, q as calculateTileMatrix, qn as createMat4f64, qr as fromValues, qt as RasterBoundsArray, ri as dot, rn as SPEC_SOURCE_TYPES, rr as extend, rt as altitudeFromMercatorZ, s as GEOJSON_TILE_LAYER_NAME, si as normalize, sn as validateStyleAndEmit, sr as getAngleDelta, st as mercatorYfromLat, ti as cross, tn as ZoomHistory, tr as ensureError, tt as unprojectFromWorldCoordinates, u as CanonicalTileID, ui as rotateZ$1, un as featureFilter, ur as getRollPitchBearing, ut as earthRadius, v as isCustomStyleLayer, vi as zero$1, vn as latest, vr as mapObject, vt as SubdivisionGranularitySetting, w as getOverlapMode, wi as fromScaling, wn as getArrayBuffer, wr as radiansToDegrees, wt as isHeatmapStyleLayer, x as isSymbolStyleLayer, xi as create$1, xn as Evented, xr as parseCacheControl, xt as Texture, y as validateCustomStyleLayer, yi as clone$1, yn as ErrorEvent, yr as mod, yt as isColorReliefStyleLayer, zi as invert, zn as angleToRotateBetweenVectors2D, zr as pixelsToTileUnits, zt as UniformMatrix4f } from "./maplibre-gl-shared-dev.mjs";
6
6
  //#region package.json
7
- var version$2 = "6.11.0";
7
+ var version$2 = "6.11.1";
8
8
  //#endregion
9
9
  //#region src/util/browser.ts
10
10
  let linkEl;
@@ -198,6 +198,92 @@ function isTimeFrozen() {
198
198
  }
199
199
  //#endregion
200
200
  //#region src/util/dom.ts
201
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
202
+ const ALLOWED_TAGS = /* @__PURE__ */ new Set([
203
+ "a",
204
+ "abbr",
205
+ "b",
206
+ "bdi",
207
+ "bdo",
208
+ "br",
209
+ "cite",
210
+ "code",
211
+ "del",
212
+ "div",
213
+ "em",
214
+ "i",
215
+ "img",
216
+ "ins",
217
+ "kbd",
218
+ "li",
219
+ "mark",
220
+ "ol",
221
+ "p",
222
+ "q",
223
+ "s",
224
+ "samp",
225
+ "small",
226
+ "span",
227
+ "strong",
228
+ "sub",
229
+ "sup",
230
+ "time",
231
+ "u",
232
+ "ul",
233
+ "var",
234
+ "wbr"
235
+ ]);
236
+ const ALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
237
+ "alt",
238
+ "class",
239
+ "datetime",
240
+ "dir",
241
+ "height",
242
+ "href",
243
+ "hreflang",
244
+ "lang",
245
+ "referrerpolicy",
246
+ "rel",
247
+ "role",
248
+ "src",
249
+ "target",
250
+ "title",
251
+ "translate",
252
+ "type",
253
+ "width"
254
+ ]);
255
+ const URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]);
256
+ const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set([
257
+ "http:",
258
+ "https:",
259
+ "mailto:"
260
+ ]);
261
+ /**
262
+ * Relative URLs have to resolve against something before their protocol can be read. Only the protocol of the
263
+ * result is used, so the base never leaks into the sanitized markup.
264
+ */
265
+ const RELATIVE_URL_BASE = "https://maplibre.invalid/";
266
+ let elementInternals;
267
+ /**
268
+ * The `Element` members the sanitizer uses, read off the prototype rather than off the element it is looking at.
269
+ * A form control named `children`, `remove` or `localName` shadows the same-named property of its own form element,
270
+ * which would otherwise let untrusted markup pick what the sanitizer walking that tree sees.
271
+ *
272
+ * They are read on first use rather than when this module is loaded, since the module is also imported where there
273
+ * is no DOM at all, such as during server side rendering.
274
+ */
275
+ function getElementInternals() {
276
+ elementInternals ??= {
277
+ getAttribute: Element.prototype.getAttribute,
278
+ getAttributeNames: Element.prototype.getAttributeNames,
279
+ querySelectorAll: Element.prototype.querySelectorAll,
280
+ remove: Element.prototype.remove,
281
+ removeAttribute: Element.prototype.removeAttribute,
282
+ localName: Object.getOwnPropertyDescriptor(Element.prototype, "localName").get,
283
+ namespaceURI: Object.getOwnPropertyDescriptor(Element.prototype, "namespaceURI").get
284
+ };
285
+ return elementInternals;
286
+ }
201
287
  var DOM = class DOM {
202
288
  static {
203
289
  this.docStyle = typeof window !== "undefined" && window.document?.documentElement.style;
@@ -257,55 +343,54 @@ var DOM = class DOM {
257
343
  return points;
258
344
  }
259
345
  /**
260
- * Sanitize an HTML string - this might not be enough to prevent all XSS attacks
261
- * Base on https://javascriptsource.com/sanitize-an-html-string-to-reduce-the-risk-of-xss-attacks/
262
- * (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
346
+ * Sanitize an untrusted HTML string, such as the attribution a remote TileJSON asks the map to display.
347
+ *
348
+ * Only the elements in `ALLOWED_TAGS` and the attributes in `ALLOWED_ATTRIBUTES` survive; anything else is
349
+ * dropped along with its subtree. An allow list is used rather than a list of known-dangerous markup because
350
+ * the latter silently permits whatever it has not heard of yet, including markup added to HTML after it was
351
+ * written.
263
352
  *
264
- * Returns the sanitized nodes rather than a string, since re-parsing serialized HTML can produce a different tree.
353
+ * The sanitized nodes are returned rather than a string: serializing and re-parsing is not a round trip, so
354
+ * a string result lets carefully nested markup mutate into a different - and no longer sanitized - tree.
265
355
  */
266
356
  static sanitize(str) {
267
- const html = new DOMParser().parseFromString(str, "text/html").body || document.createElement("body");
268
- const dangerousElements = html.querySelectorAll("script, iframe");
269
- for (const element of dangerousElements) element.remove();
270
- DOM.clean(html);
357
+ const body = new DOMParser().parseFromString(str, "text/html").body;
358
+ const { querySelectorAll, remove } = getElementInternals();
359
+ for (const element of Array.from(querySelectorAll.call(body, "*"))) if (DOM.isAllowedElement(element)) DOM.removeDisallowedAttributes(element);
360
+ else remove.call(element);
271
361
  const fragment = document.createDocumentFragment();
272
- fragment.append(...html.childNodes);
362
+ fragment.append(...body.childNodes);
273
363
  return fragment;
274
364
  }
275
365
  /**
276
- * Check if the attribute is potentially dangerous
366
+ * An element is allowed only when it is plain HTML markup on the allow list. Elements in the SVG and MathML
367
+ * namespaces are rejected even when their local name is allowed, since foreign content follows different
368
+ * parsing rules and is what makes most mutation attacks possible in the first place.
277
369
  */
278
- static isPossiblyDangerous(name, value) {
279
- const val = value.replace(/\s+/g, "").toLowerCase();
280
- if ([
281
- "src",
282
- "href",
283
- "xlink:href"
284
- ].includes(name)) {
285
- if (val.includes("javascript:") || val.includes("data:")) return true;
286
- }
287
- if (name === "srcdoc") return true;
288
- if (name.startsWith("on")) return true;
370
+ static isAllowedElement(element) {
371
+ const { namespaceURI, localName } = getElementInternals();
372
+ return namespaceURI.call(element) === HTML_NAMESPACE && ALLOWED_TAGS.has(localName.call(element));
289
373
  }
290
- /**
291
- * Remove dangerous stuff from the HTML document's nodes
292
- * @param html - The HTML document
293
- */
294
- static clean(html) {
295
- const nodes = html.children;
296
- for (const node of nodes) {
297
- DOM.removeAttributes(node);
298
- DOM.clean(node);
374
+ static removeDisallowedAttributes(element) {
375
+ const { getAttributeNames, getAttribute, removeAttribute } = getElementInternals();
376
+ for (const name of getAttributeNames.call(element)) {
377
+ if (DOM.isAllowedAttribute(name, getAttribute.call(element, name))) continue;
378
+ removeAttribute.call(element, name);
299
379
  }
300
380
  }
301
381
  /**
302
- * Remove potentially dangerous attributes from an element
303
- * @param elem - The element
382
+ * `href` and `src` are further restricted to the protocols in `ALLOWED_PROTOCOLS`, so that `javascript:` and
383
+ * `data:` URLs cannot turn an otherwise harmless link or image into a script. The protocol is taken from a
384
+ * parsed URL rather than matched against the raw string, since the URL parser is what the browser will apply
385
+ * and it ignores tabs, newlines and leading control characters that a string comparison would trip over.
304
386
  */
305
- static removeAttributes(elem) {
306
- for (const { name, value } of Array.from(elem.attributes)) {
307
- if (!DOM.isPossiblyDangerous(name, value)) continue;
308
- elem.removeAttribute(name);
387
+ static isAllowedAttribute(name, value) {
388
+ if (!ALLOWED_ATTRIBUTES.has(name) && !name.startsWith("aria-")) return false;
389
+ if (!URL_ATTRIBUTES.has(name)) return true;
390
+ try {
391
+ return ALLOWED_PROTOCOLS.has(new URL(value, RELATIVE_URL_BASE).protocol);
392
+ } catch {
393
+ return false;
309
394
  }
310
395
  }
311
396
  };
@@ -2963,6 +3048,7 @@ var VectorTileSource = class extends Evented {
2963
3048
  };
2964
3049
  params.request.collectResourceTiming = this._collectResourceTiming;
2965
3050
  await this.dispatcher.waitForInitComplete();
3051
+ if (tile.aborted) return;
2966
3052
  let messageType = "RT";
2967
3053
  if (!tile.actor || tile.state === "expired") {
2968
3054
  tile.actor = this.dispatcher.getReadyActor();
@@ -3474,18 +3560,13 @@ function applySourceDiff(updateable, diff, promoteId) {
3474
3560
  /**
3475
3561
  * Merge two GeoJSONSourceDiffs, considering the order of operations as specified above (remove, add, update).
3476
3562
  *
3477
- * For `add` features that use promoteId, the feature id will be set to the promoteId value temporarily so that
3478
- * the merge can be completed, then reverted to the original promoteId state after the merge.
3563
+ * `add` features of a promoteId source carry their id in a property, so `promoteId` is needed to key them.
3479
3564
  */
3480
3565
  function mergeSourceDiffs(prevDiff, nextDiff, promoteId) {
3481
3566
  if (!prevDiff) return nextDiff || {};
3482
3567
  if (!nextDiff) return prevDiff || {};
3483
- if (promoteId) {
3484
- promoteFeatureIds(prevDiff.add, promoteId);
3485
- promoteFeatureIds(nextDiff.add, promoteId);
3486
- }
3487
- const prev = diffToHashed(prevDiff);
3488
- const next = diffToHashed(nextDiff);
3568
+ const prev = diffToHashed(prevDiff, promoteId);
3569
+ const next = diffToHashed(nextDiff, promoteId);
3489
3570
  resolveMergeConflicts(prev, next);
3490
3571
  const merged = {};
3491
3572
  if (prev.removeAll || next.removeAll) merged.removeAll = true;
@@ -3493,9 +3574,7 @@ function mergeSourceDiffs(prevDiff, nextDiff, promoteId) {
3493
3574
  merged.add = new Map([...prev.add, ...next.add]);
3494
3575
  merged.update = new Map([...prev.update, ...next.update]);
3495
3576
  if (merged.remove.size && merged.add.size) for (const id of merged.add.keys()) merged.remove.delete(id);
3496
- const mergedDiff = hashedToDiff(merged);
3497
- if (promoteId) demoteFeatureIds(mergedDiff.add, promoteId);
3498
- return mergedDiff;
3577
+ return hashedToDiff(merged);
3499
3578
  }
3500
3579
  /**
3501
3580
  * Resolve merge conflicts between two GeoJSONSourceDiffs considering the ordering above (remove/add/update).
@@ -3542,32 +3621,16 @@ function mergeFeatureDiffs(prev, next) {
3542
3621
  return merged;
3543
3622
  }
3544
3623
  /**
3545
- * Mutates diff.add and applies a feature id using the promoteId property
3546
- */
3547
- function promoteFeatureIds(add, promoteId) {
3548
- if (!add) return;
3549
- for (const feature of add) {
3550
- const id = getFeatureId(feature, promoteId);
3551
- if (id != null) feature.id = id;
3552
- }
3553
- }
3554
- /**
3555
- * Mutates diff.add and removes the feature id if using the promoteId property
3556
- */
3557
- function demoteFeatureIds(add, promoteId) {
3558
- if (!add) return;
3559
- for (const feature of add) if (getFeatureId(feature, promoteId) != null) delete feature.id;
3560
- }
3561
- /**
3562
3624
  * @internal
3563
3625
  * Convert a GeoJSONSourceDiff to an idempotent hashed representation using Sets and Maps
3626
+ * @param promoteId - If set, `add` features are keyed by that property instead of by `id`.
3564
3627
  */
3565
- function diffToHashed(diff) {
3628
+ function diffToHashed(diff, promoteId) {
3566
3629
  if (!diff) return {};
3567
3630
  const hashed = {};
3568
3631
  hashed.removeAll = diff.removeAll;
3569
3632
  hashed.remove = new Set(diff.remove || []);
3570
- hashed.add = new Map(diff.add?.map((feature) => [feature.id, feature]));
3633
+ hashed.add = new Map(diff.add?.map((feature) => [getFeatureId(feature, promoteId), feature]));
3571
3634
  hashed.update = new Map(diff.update?.map((update) => [update.id, update]));
3572
3635
  return hashed;
3573
3636
  }
@@ -3718,7 +3781,7 @@ var GeoJSONSource = class extends Evented {
3718
3781
  maxZoom: this.maxzoom,
3719
3782
  lineMetrics: options.lineMetrics || false,
3720
3783
  generateId: options.generateId || false,
3721
- promoteId: typeof options.promoteId === "string" ? options.promoteId : void 0,
3784
+ promoteId: this._promoteIdKey,
3722
3785
  cluster: options.cluster || false,
3723
3786
  clusterOptions: {
3724
3787
  maxZoom: this._getClusterMaxZoom(options.clusterMaxZoom),
@@ -3733,6 +3796,10 @@ var GeoJSONSource = class extends Evented {
3733
3796
  filter: options.filter
3734
3797
  }, options.workerOptions);
3735
3798
  }
3799
+ /** The `promoteId` property name, when it is a plain string. */
3800
+ get _promoteIdKey() {
3801
+ return typeof this.promoteId === "string" ? this.promoteId : void 0;
3802
+ }
3736
3803
  _hasPendingWorkerUpdate() {
3737
3804
  return this._pendingWorkerUpdate.data !== void 0 || this._pendingWorkerUpdate.diff !== void 0 || this._pendingWorkerUpdate.updateCluster;
3738
3805
  }
@@ -3779,7 +3846,7 @@ var GeoJSONSource = class extends Evented {
3779
3846
  * @param diff - The changes that need to be applied.
3780
3847
  */
3781
3848
  updateData(diff) {
3782
- this._pendingWorkerUpdate.diff = mergeSourceDiffs(this._pendingWorkerUpdate.diff, diff);
3849
+ this._pendingWorkerUpdate.diff = mergeSourceDiffs(this._pendingWorkerUpdate.diff, diff, this._promoteIdKey);
3783
3850
  return this._updateWorkerData();
3784
3851
  }
3785
3852
  /**
@@ -4011,7 +4078,7 @@ var GeoJSONSource = class extends Evented {
4011
4078
  */
4012
4079
  _applyDiffToSource(diff) {
4013
4080
  if (!diff) return;
4014
- const promoteId = typeof this.promoteId === "string" ? this.promoteId : void 0;
4081
+ const promoteId = this._promoteIdKey;
4015
4082
  if (!this._data.url && !this._data.updateable) {
4016
4083
  const updateable = toUpdateable(this._data.geojson, promoteId);
4017
4084
  if (!updateable) throw new Error(`GeoJSONSource "${this.id}": GeoJSON data is not compatible with updateData`);
@@ -22544,7 +22611,10 @@ var HandlerManager = class {
22544
22611
  return this._el?.ownerDocument?.defaultView || window;
22545
22612
  }
22546
22613
  constructor(map, camera, options) {
22547
- this._terrainGestureAnchorElevation = null;
22614
+ this._terrainGesture = {
22615
+ inFlight: false,
22616
+ anchorElevation: null
22617
+ };
22548
22618
  this.handleWindowEvent = (e) => {
22549
22619
  this.handleEvent(e, `${e.type}Window`);
22550
22620
  };
@@ -22729,7 +22799,7 @@ var HandlerManager = class {
22729
22799
  const touchPitch = map.touchPitch = new TwoFingersTouchPitchHandler(map);
22730
22800
  this._add("touchPitch", touchPitch);
22731
22801
  if (options.interactive && options.touchPitch) map.touchPitch.enable(options.touchPitch);
22732
- const getCenter = () => map.project(map.getCenter());
22802
+ const getCenter = () => this._camera.transform.centerPoint;
22733
22803
  const mouseRotate = generateMouseRotationHandler(options, getCenter);
22734
22804
  const mousePitch = generateMousePitchHandler(options);
22735
22805
  const mouseRoll = generateMouseRollHandler(options, getCenter);
@@ -22833,14 +22903,18 @@ var HandlerManager = class {
22833
22903
  this._updateMapTransform(combined, combinedEventsInProgress, combinedDeactivatedHandlers);
22834
22904
  this._changes = [];
22835
22905
  }
22906
+ /**
22907
+ * Applies a frame's combined handler deltas to the requested camera state and fires the movement
22908
+ * events. A frame without a change only fires the events and takes no requested camera state, so a
22909
+ * trailing frame that merely ends a handler leaves none behind for the next gesture to start from.
22910
+ */
22836
22911
  _updateMapTransform(combinedResult, combinedEventsInProgress, deactivatedHandlers) {
22837
- const map = this._map;
22838
- const tr = this._camera.getTransformForUpdate();
22839
- const terrain = map.terrain;
22840
- if (!hasChange(combinedResult) && !(terrain && this._terrainMovement)) {
22912
+ const terrain = this._map.terrain;
22913
+ if (!hasChange(combinedResult)) {
22841
22914
  this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true);
22842
22915
  return;
22843
22916
  }
22917
+ const tr = this._camera.getTransformForUpdate();
22844
22918
  this._camera.stop(true);
22845
22919
  const { panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta } = combinedResult;
22846
22920
  let { around, aroundOnSurface } = this._resolveAround(combinedResult, terrain, tr);
@@ -22895,12 +22969,12 @@ var HandlerManager = class {
22895
22969
  */
22896
22970
  _terrainGestureElevation(terrain, around, aroundOnSurface, tr, combinedEventsInProgress) {
22897
22971
  if (!aroundOnSurface) return;
22898
- if (!this._terrainMovement && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
22972
+ if (this._terrainGesture.anchorElevation === null && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
22899
22973
  const anchor = tr.screenTerrainPointToMercatorCoordinate(around, terrain);
22900
- this._terrainGestureAnchorElevation = anchor ? anchor.z : null;
22974
+ this._terrainGesture.anchorElevation = anchor ? anchor.z : void 0;
22901
22975
  }
22902
- if (this._terrainGestureAnchorElevation === null) return;
22903
- const elevation = this._terrainGestureAnchorElevation;
22976
+ const elevation = this._terrainGesture.anchorElevation;
22977
+ if (elevation === null || elevation === void 0) return;
22904
22978
  if (around.distSqr(tr.centerPoint) < .01) return;
22905
22979
  if (elevation - tr.elevation >= TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION * (tr.getCameraAltitude() - tr.elevation)) return;
22906
22980
  return elevation;
@@ -22924,21 +22998,13 @@ var HandlerManager = class {
22924
22998
  cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc);
22925
22999
  return;
22926
23000
  }
22927
- if (cameraHelper.useGlobeControls) {
22928
- if (!this._terrainMovement && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
22929
- this._terrainMovement = true;
22930
- this._camera.elevationFreeze = true;
22931
- }
22932
- cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc);
22933
- return;
22934
- }
22935
- if (!this._terrainMovement && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
22936
- this._terrainMovement = true;
23001
+ if (!this._terrainGesture.inFlight) {
23002
+ this._terrainGesture.inFlight = true;
22937
23003
  this._camera.elevationFreeze = true;
22938
23004
  cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc);
22939
23005
  return;
22940
23006
  }
22941
- if (deltasForHelper.aroundElevation === void 0 && combinedEventsInProgress.drag && this._terrainMovement && panDelta) {
23007
+ if (!cameraHelper.useGlobeControls && deltasForHelper.aroundElevation === void 0 && combinedEventsInProgress.drag && panDelta) {
22942
23008
  tr.setCenter(tr.screenPointToLocation(tr.centerPoint.sub(panDelta)));
22943
23009
  return;
22944
23010
  }
@@ -22973,10 +23039,12 @@ var HandlerManager = class {
22973
23039
  for (const name in endEvents) this._fireEvent(name, endEvents[name]);
22974
23040
  const stillMoving = isMoving(this._eventsInProgress);
22975
23041
  const finishedMoving = (wasMoving || nowMoving) && !stillMoving;
22976
- if (finishedMoving && this._terrainMovement) {
23042
+ if (finishedMoving && this._terrainGesture.inFlight) {
22977
23043
  this._camera.elevationFreeze = false;
22978
- this._terrainMovement = false;
22979
- this._terrainGestureAnchorElevation = null;
23044
+ this._terrainGesture = {
23045
+ inFlight: false,
23046
+ anchorElevation: null
23047
+ };
22980
23048
  const tr = this._camera.getTransformForUpdate();
22981
23049
  if (this._map.getCenterClampedToGround()) tr.recalculateZoomAndCenter(this._map.terrain);
22982
23050
  this._camera.applyUpdatedTransform(tr);
@@ -22990,14 +23058,19 @@ var HandlerManager = class {
22990
23058
  inertialEase.freezeElevation = true;
22991
23059
  this._map.easeTo(inertialEase, { originalEvent: originalEndEvent });
22992
23060
  } else {
22993
- this._map.fire(new MapMovementEvent("moveend", { originalEvent: originalEndEvent }));
23061
+ this._fireEvent("moveend", originalEndEvent);
22994
23062
  if (shouldSnapToNorth(this._map.getBearing())) this._map.resetNorth();
22995
23063
  }
22996
23064
  this._updatingCamera = false;
22997
23065
  }
22998
23066
  }
23067
+ /**
23068
+ * Fires a movement event through the camera, whose evented parent is the map: the map's listeners
23069
+ * see the event as before, and the camera's own `moveend` listener drops the requested camera state,
23070
+ * so the next gesture starts from the rendered transform instead of the state this one ended in.
23071
+ */
22999
23072
  _fireEvent(type, e) {
23000
- this._map.fire(new MapMovementEvent(type, e ? { originalEvent: e } : {}));
23073
+ this._camera.fire(new MapMovementEvent(type, e ? { originalEvent: e } : {}));
23001
23074
  }
23002
23075
  _requestFrame() {
23003
23076
  this._map.triggerRepaint();
@@ -23044,6 +23117,15 @@ var Camera = class extends Evented {
23044
23117
  delete this._requestedCameraState;
23045
23118
  });
23046
23119
  }
23120
+ /**
23121
+ * @internal
23122
+ * Hands the camera the map's terrain, or null when the map has none, and brings the center
23123
+ * elevation up to date with it, see {@link Camera.applyTerrainChange}.
23124
+ */
23125
+ setTerrain(terrain) {
23126
+ this.terrain = terrain;
23127
+ this.applyTerrainChange();
23128
+ }
23047
23129
  migrateProjection(newTransform, newCameraHelper) {
23048
23130
  newTransform.apply(this.transform, true);
23049
23131
  this.transform = newTransform;
@@ -23348,6 +23430,7 @@ var Camera = class extends Evented {
23348
23430
  this._fireMoveEvents(eventData);
23349
23431
  }, (interruptingEaseId) => {
23350
23432
  if (this.terrain && options.freezeElevation) this._finalizeElevation();
23433
+ else this.elevationFreeze = false;
23351
23434
  this._afterEase(eventData, interruptingEaseId);
23352
23435
  }, options);
23353
23436
  return this;
@@ -23384,6 +23467,23 @@ var Camera = class extends Evented {
23384
23467
  }
23385
23468
  /**
23386
23469
  * @internal
23470
+ * Applies a change of the terrain under the center to the transform: the terrain was set or
23471
+ * removed, or a DEM tile landed. The center keeps its place and the camera moves with the
23472
+ * center's elevation, as it does on every rendered frame while nothing holds the elevation.
23473
+ * While a gesture or an ease holds it this does nothing: the camera stays where the user put
23474
+ * it and the hold's end re-solves zoom and center onto the new terrain without moving it.
23475
+ * Nothing is in flight when this writes, so it writes the rendered transform, like the
23476
+ * per-frame clamp; a requested camera state created here would outlive the call and the
23477
+ * next gesture would start from it.
23478
+ */
23479
+ applyTerrainChange() {
23480
+ if (this.elevationFreeze) return;
23481
+ const tr = this.transform;
23482
+ tr.setMinElevationForCurrentTile(this.terrain ? this.terrain.getMinTileElevationForLngLatZoom(tr.center, tr.tileZoom) : 0);
23483
+ if (this.getCenterClampedToGround()) tr.setElevation(this.terrain ? this.terrain.getElevationForLngLat(tr.center, tr) : 0);
23484
+ }
23485
+ /**
23486
+ * @internal
23387
23487
  * Called when the camera is about to be manipulated.
23388
23488
  * If `transformCameraUpdate` is specified or terrain is enabled, a copy of
23389
23489
  * the current transform is created to track the accumulated changes.
@@ -23583,6 +23683,7 @@ var Camera = class extends Evented {
23583
23683
  this._fireMoveEvents(eventData);
23584
23684
  }, () => {
23585
23685
  if (this.terrain && options.freezeElevation) this._finalizeElevation();
23686
+ else this.elevationFreeze = false;
23586
23687
  this._afterEase(eventData);
23587
23688
  }, options);
23588
23689
  return this;
@@ -26155,9 +26256,7 @@ var Map$1 = class extends Evented {
26155
26256
  this.terrain = null;
26156
26257
  this.painter.renderToTexture = null;
26157
26258
  this.painter.destroyRTTResources();
26158
- this._camera.terrain = null;
26159
- this._camera.transform.setMinElevationForCurrentTile(0);
26160
- if (this.getCenterClampedToGround()) this._camera.transform.setElevation(0);
26259
+ this._camera.setTerrain(null);
26161
26260
  } else {
26162
26261
  const tileManager = this.style.tileManagers[options.source];
26163
26262
  if (!tileManager) throw new Error(`cannot load terrain, because there exists no source with ID: ${options.source}`);
@@ -26170,9 +26269,7 @@ var Map$1 = class extends Evented {
26170
26269
  if (this.terrain) this.terrain.destroy();
26171
26270
  this.terrain = new Terrain(this.painter, tileManager, options, this._terrainSkirtLength);
26172
26271
  this.painter.renderToTexture = new RenderToTexture(this.painter, this.terrain);
26173
- this._camera.terrain = this.terrain;
26174
- this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
26175
- this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform));
26272
+ this._camera.setTerrain(this.terrain);
26176
26273
  this._terrainDataCallback = (e) => this._handleTerrainDataEvent(e, options.source);
26177
26274
  this.style.on("data", this._terrainDataCallback);
26178
26275
  }
@@ -26190,10 +26287,9 @@ var Map$1 = class extends Evented {
26190
26287
  this.terrain.resetElevationCache();
26191
26288
  this.style.triggerSymbolPlacement();
26192
26289
  }
26193
- if (isTerrainSourceEvent && event.tile) this.painter.markTerrainDepthDirty();
26194
- if (isTerrainSourceEvent && event.tile && !this._camera.elevationFreeze) {
26195
- this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom));
26196
- if (this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform));
26290
+ if (isTerrainSourceEvent && event.tile) {
26291
+ this.painter.markTerrainDepthDirty();
26292
+ this._camera.applyTerrainChange();
26197
26293
  }
26198
26294
  if (!event.tile) return;
26199
26295
  if (event.source?.type === "image") {