vendor: 1,244,733 bytes, lines 1-30240
1/** 2* MapLibre GL JS 3* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.11.1/LICENSE.txt 4*/ 5import { $ 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//#region package.json 7var version$2 = "6.11.1"; 8//#endregion 9//#region src/util/browser.ts 10let linkEl; 11let reducedMotionQuery; 12let reducedMotionOverride; 13/** */ 14const browser = { 15 /** 16 * Schedules a callback to be invoked on the next animation frame. 17 * @param abortController - Controller to abort the scheduled frame. 18 * @param fn - Callback to invoke with the paint start timestamp. 19 * @param reject - Callback to invoke if the frame is aborted. 20 * @param targetWindow - Optional window to use for requestAnimationFrame. 21 * When the map is rendered in a popup window or iframe, pass the owning 22 * window to ensure animation frames continue even when the main window 23 * is not focused. 24 */ 25 frame(abortController, fn, reject, targetWindow) { 26 const win = targetWindow || window; 27 const frameId = win.requestAnimationFrame((paintStartTimestamp) => { 28 unsubscribe(); 29 fn(paintStartTimestamp); 30 }); 31 const { unsubscribe } = subscribe(abortController.signal, "abort", () => { 32 unsubscribe(); 33 win.cancelAnimationFrame(frameId); 34 reject(new AbortError(abortController.signal.reason)); 35 }, false); 36 }, 37 /** 38 * Returns a promise that resolves on the next animation frame. 39 * @param abortController - Controller to abort the scheduled frame. 40 * @param targetWindow - Optional window to use for requestAnimationFrame. 41 * @see {@link browser.frame} 42 */ 43 frameAsync(abortController, targetWindow) { 44 return new Promise((resolve, reject) => { 45 this.frame(abortController, resolve, reject, targetWindow); 46 }); 47 }, 48 getImageData(img, padding = 0) { 49 return this.getImageCanvasContext(img).getImageData(-padding, -padding, img.width + 2 * padding, img.height + 2 * padding); 50 }, 51 /** 52 * Draws an image into a 2D canvas whose pixels can be read back cheaply. An `OffscreenCanvas` 53 * is used where the browser has one that reads back faithfully: drawing a GPU-backed 54 * `ImageBitmap` into a document canvas opened with `willReadFrequently` stalls the main thread 55 * for tens of milliseconds on GPU-accelerated browsers, an `OffscreenCanvas` does not. 56 */ 57 getImageCanvasContext(img) { 58 let context; 59 if (offscreenCanvasSupported() && !isOffscreenCanvasDistorted()) context = new OffscreenCanvas(img.width, img.height).getContext("2d", { willReadFrequently: true }); 60 else { 61 const canvas = window.document.createElement("canvas"); 62 canvas.width = img.width; 63 canvas.height = img.height; 64 context = canvas.getContext("2d", { willReadFrequently: true }); 65 } 66 if (!context) throw new Error("failed to create canvas 2d context"); 67 context.drawImage(img, 0, 0, img.width, img.height); 68 return context; 69 }, 70 resolveURL(path) { 71 linkEl ||= document.createElement("a"); 72 linkEl.href = path; 73 return linkEl.href; 74 }, 75 get hardwareConcurrency() { 76 return typeof navigator !== "undefined" && navigator.hardwareConcurrency || 4; 77 }, 78 get prefersReducedMotion() { 79 if (reducedMotionOverride !== void 0) return reducedMotionOverride; 80 if (!matchMedia) return false; 81 reducedMotionQuery ??= matchMedia("(prefers-reduced-motion: reduce)"); 82 return reducedMotionQuery.matches; 83 }, 84 set prefersReducedMotion(value) { 85 reducedMotionOverride = value; 86 } 87}; 88//#endregion 89//#region src/util/time_control.ts 90/** 91* Manages time flow with optional freezing capability for deterministic rendering. 92*/ 93var TimeManager = class { 94 constructor() { 95 this._frozenAt = null; 96 } 97 /** 98 * Gets the current time, either real or frozen. 99 * @returns Current time in milliseconds 100 */ 101 getCurrentTime() { 102 return this._frozenAt !== null ? this._frozenAt : performance.now(); 103 } 104 /** 105 * Sets time at a specific timestamp. 106 * @param timestamp - Time in milliseconds to set 107 */ 108 setNow(timestamp) { 109 this._frozenAt = timestamp; 110 } 111 /** 112 * Restores normal time flow. 113 */ 114 restoreNow() { 115 this._frozenAt = null; 116 } 117 /** 118 * Returns whether time is currently frozen. 119 * @returns True if time is frozen, false otherwise 120 */ 121 isFrozen() { 122 return this._frozenAt !== null; 123 } 124}; 125const timeManager = new TimeManager(); 126/** 127* Returns the current time in milliseconds. 128* When time is frozen via setNow(), returns the frozen timestamp. 129* Otherwise returns real browser time via performance.now(). 130* 131* @returns Current time in milliseconds 132* @example 133* ```ts 134* // Measure elapsed time 135* const start = maplibregl.now(); 136* // ... later ... 137* const elapsed = maplibregl.now() - start; 138* 139* // During frozen time 140* maplibregl.setNow(16.67); 141* console.log(maplibregl.now()); // 16.67 142* maplibregl.restoreNow(); 143* console.log(maplibregl.now()); // real time 144* ``` 145*/ 146function now() { 147 return timeManager.getCurrentTime(); 148} 149/** 150* Freezes time at a specific timestamp for deterministic rendering. 151* Useful for frame-by-frame video capture where each frame needs 152* a consistent time value. 153* 154* @param timestamp - Time in milliseconds to freeze at 155* @example 156* ```ts 157* // Freeze time for video export at 60fps 158* setNow(0); // First frame 159* // ... render frame ... 160* setNow(16.67); // Second frame 161* // ... render frame ... 162* setNow(33.34); // Third frame 163* // ... done ... 164* restoreNow(); // Resume normal time 165* ``` 166*/ 167function setNow(timestamp) { 168 timeManager.setNow(timestamp); 169} 170/** 171* Restores normal time flow after freezing with setNow(). 172* Call this after finishing deterministic rendering operations. 173* 174* @example 175* ```ts 176* // After video export, resume normal time 177* setNow(0); 178* // ... export frames ... 179* restoreNow(); // Map animations resume normally 180* ``` 181*/ 182function restoreNow() { 183 timeManager.restoreNow(); 184} 185/** 186* Returns whether time is currently frozen. 187* @returns True if time is frozen via setNow(), false otherwise 188* @example 189* ```ts 190* setNow(1000); 191* console.log(isTimeFrozen()); // true 192* restoreNow(); 193* console.log(isTimeFrozen()); // false 194* ``` 195*/ 196function isTimeFrozen() { 197 return timeManager.isFrozen(); 198} 199//#endregion 200//#region src/util/dom.ts 201const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; 202const 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]); 236const 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]); 255const URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]); 256const 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*/ 265const RELATIVE_URL_BASE = "https://maplibre.invalid/"; 266let 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*/ 275function 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} 287var DOM = class DOM { 288 static { 289 this.docStyle = typeof window !== "undefined" && window.document?.documentElement.style; 290 } 291 static { 292 this.selectProp = !DOM.docStyle || "userSelect" in DOM.docStyle ? "userSelect" : "webkitUserSelect"; 293 } 294 static create(tagName, className, container) { 295 const el = window.document.createElement(tagName); 296 if (className !== void 0) el.className = className; 297 if (container) container.appendChild(el); 298 return el; 299 } 300 static createNS(namespaceURI, tagName) { 301 return window.document.createElementNS(namespaceURI, tagName); 302 } 303 static disableDrag() { 304 if (DOM.docStyle && DOM.selectProp) { 305 DOM.userSelect = DOM.docStyle[DOM.selectProp]; 306 DOM.docStyle[DOM.selectProp] = "none"; 307 } 308 } 309 static enableDrag() { 310 if (DOM.docStyle && DOM.selectProp) DOM.docStyle[DOM.selectProp] = DOM.userSelect; 311 } 312 static suppressClickInternal(e) { 313 e.preventDefault(); 314 e.stopPropagation(); 315 window.removeEventListener("click", DOM.suppressClickInternal, true); 316 } 317 static suppressClick() { 318 window.addEventListener("click", DOM.suppressClickInternal, true); 319 window.setTimeout(() => { 320 window.removeEventListener("click", DOM.suppressClickInternal, true); 321 }, 0); 322 } 323 static getScale(element) { 324 const rect = element.getBoundingClientRect(); 325 return { 326 x: rect.width / element.offsetWidth || 1, 327 y: rect.height / element.offsetHeight || 1, 328 boundingClientRect: rect 329 }; 330 } 331 static getPoint(el, scale, e) { 332 const rect = scale.boundingClientRect; 333 return new Point((e.clientX - rect.left) / scale.x - el.clientLeft, (e.clientY - rect.top) / scale.y - el.clientTop); 334 } 335 static mousePos(el, e) { 336 const scale = DOM.getScale(el); 337 return DOM.getPoint(el, scale, e); 338 } 339 static touchPos(el, touches) { 340 const points = []; 341 const scale = DOM.getScale(el); 342 for (const touch of touches) points.push(DOM.getPoint(el, scale, touch)); 343 return points; 344 } 345 /** 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. 352 * 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. 355 */ 356 static sanitize(str) { 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); 361 const fragment = document.createDocumentFragment(); 362 fragment.append(...body.childNodes); 363 return fragment; 364 } 365 /** 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. 369 */ 370 static isAllowedElement(element) { 371 const { namespaceURI, localName } = getElementInternals(); 372 return namespaceURI.call(element) === HTML_NAMESPACE && ALLOWED_TAGS.has(localName.call(element)); 373 } 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); 379 } 380 } 381 /** 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. 386 */ 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; 394 } 395 } 396}; 397//#endregion 398//#region src/util/image_request.ts 399let ImageRequest; 400(function(_ImageRequest) { 401 let imageRequestQueue; 402 let currentParallelImageRequests; 403 let throttleControlCallbackHandleCounter; 404 let throttleControlCallbacks; 405 _ImageRequest.resetRequestQueue = () => { 406 imageRequestQueue = []; 407 currentParallelImageRequests = 0; 408 throttleControlCallbackHandleCounter = 0; 409 throttleControlCallbacks = {}; 410 }; 411 _ImageRequest.addThrottleControl = (callback) => { 412 const handle = throttleControlCallbackHandleCounter++; 413 throttleControlCallbacks[handle] = callback; 414 return handle; 415 }; 416 _ImageRequest.removeThrottleControl = (callbackHandle) => { 417 delete throttleControlCallbacks[callbackHandle]; 418 processQueue(); 419 }; 420 /** 421 * Check to see if any of the installed callbacks are requesting the queue 422 * to be throttled. 423 * @returns `true` if any callback is causing the queue to be throttled. 424 */ 425 const isThrottled = () => { 426 for (const key of Object.keys(throttleControlCallbacks)) if (throttleControlCallbacks[key]()) return true; 427 return false; 428 }; 429 async function transformAndGetImage(requestManager, url, resourceType, abortController, supportImageRefresh = true, imageBitmapOptions) { 430 const requestParameters = await requestManager.transformRequest(url, resourceType); 431 throwIfAborted(abortController.signal); 432 return ImageRequest.getImage(requestParameters, abortController, supportImageRefresh, imageBitmapOptions); 433 } 434 _ImageRequest.transformAndGetImage = transformAndGetImage; 435 _ImageRequest.getImage = (requestParameters, abortController, supportImageRefresh = true, imageBitmapOptions) => { 436 return new Promise((resolve, reject) => { 437 requestParameters.headers ||= {}; 438 requestParameters.headers.accept = "image/webp,*/*"; 439 extend(requestParameters, { type: "image" }); 440 const request = { 441 abortController, 442 requestParameters, 443 supportImageRefresh, 444 imageBitmapOptions, 445 state: "queued", 446 onError: (error) => { 447 reject(error); 448 }, 449 onSuccess: (response) => { 450 resolve(response); 451 } 452 }; 453 imageRequestQueue.push(request); 454 processQueue(); 455 }); 456 }; 457 const arrayBufferToCanvasImageSource = (data, imageBitmapOptions) => { 458 if (typeof createImageBitmap === "function") return arrayBufferToImageBitmap(data, imageBitmapOptions); 459 else return arrayBufferToImage(data); 460 }; 461 /** 462 * Runs one queued image request. 463 * 464 * An empty response body (e.g. HTTP 204 for a tile without content) resolves with 465 * `data: null`, keeping the expiry headers, so callers can handle the absence 466 * explicitly and re-request when the headers say so. 467 */ 468 const doImageRequest = async (itemInQueue) => { 469 itemInQueue.state = "running"; 470 const { requestParameters, supportImageRefresh, imageBitmapOptions, onError, onSuccess, abortController } = itemInQueue; 471 const canUseHTMLImageElement = supportImageRefresh === false && !imageBitmapOptions && !isWorker(self) && !getProtocol(requestParameters.url) && (!requestParameters.headers || Object.keys(requestParameters.headers).reduce((acc, item) => acc && item === "accept", true)); 472 currentParallelImageRequests++; 473 const getImagePromise = canUseHTMLImageElement ? getImageUsingHtmlImage(requestParameters, abortController) : makeRequest(requestParameters, abortController); 474 try { 475 const response = await getImagePromise; 476 delete itemInQueue.abortController; 477 itemInQueue.state = "completed"; 478 if (response.data instanceof HTMLImageElement || isImageBitmap(response.data)) onSuccess(response); 479 else if (!response.data || response.data.byteLength === 0) onSuccess({ 480 data: null, 481 cacheControl: response.cacheControl, 482 expires: response.expires 483 }); 484 else onSuccess({ 485 data: await arrayBufferToCanvasImageSource(response.data, imageBitmapOptions), 486 cacheControl: response.cacheControl, 487 expires: response.expires 488 }); 489 } catch (err) { 490 delete itemInQueue.abortController; 491 onError(ensureError(err)); 492 } finally { 493 currentParallelImageRequests--; 494 processQueue(); 495 } 496 }; 497 /** 498 * Process some number of items in the image request queue. 499 */ 500 const processQueue = () => { 501 const maxImageRequests = isThrottled() ? config.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME : config.MAX_PARALLEL_IMAGE_REQUESTS; 502 for (let numImageRequests = currentParallelImageRequests; numImageRequests < maxImageRequests && imageRequestQueue.length > 0; numImageRequests++) { 503 const topItemInQueue = imageRequestQueue.shift(); 504 if (topItemInQueue.abortController.signal.aborted) { 505 numImageRequests--; 506 continue; 507 } 508 doImageRequest(topItemInQueue); 509 } 510 }; 511 const getImageUsingHtmlImage = (requestParameters, abortController) => { 512 return new Promise((resolve, reject) => { 513 const image = new Image(); 514 const url = requestParameters.url; 515 const credentials = requestParameters.credentials; 516 if (credentials && credentials === "include") image.crossOrigin = "use-credentials"; 517 else if (credentials && credentials === "same-origin" || !sameOrigin(url)) image.crossOrigin = "anonymous"; 518 abortController.signal.addEventListener("abort", () => { 519 image.src = ""; 520 reject(new AbortError(abortController.signal.reason)); 521 }); 522 image.fetchPriority = "high"; 523 image.onload = () => { 524 image.onerror = image.onload = null; 525 resolve({ data: image }); 526 }; 527 image.onerror = () => { 528 image.onerror = image.onload = null; 529 if (abortController.signal.aborted) return; 530 reject(/* @__PURE__ */ new Error("Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.")); 531 }; 532 image.src = url; 533 }); 534 }; 535})(ImageRequest || (ImageRequest = {})); 536ImageRequest.resetRequestQueue(); 537var RequestManager = class { 538 constructor(transformRequestFn) { 539 this._transformRequestFn = transformRequestFn ?? null; 540 } 541 transformRequest(url, type) { 542 if (this._transformRequestFn) return this._transformRequestFn(url, type) || { url }; 543 return { url }; 544 } 545 setTransformRequest(transformRequest) { 546 this._transformRequestFn = transformRequest; 547 } 548}; 549//#endregion 550//#region src/ui/events.ts 551/** 552* The base event for MapLibre 553* 554* @group Event Related 555*/ 556var MapLibreEvent = class extends Event {}; 557/** 558* `MapMovementEvent` is the event type for the camera-transition map events: 559* `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `rotatestart`, `rotate`, `rotateend`, 560* `dragstart`, `drag`, `dragend`, `pitchstart`, `pitch`, `pitchend`, `rollstart`, `roll` and `rollend`. 561* These are fired as the map's view changes, as a result of either user interaction or methods such 562* as {@link Map.jumpTo} / {@link Map.flyTo}. 563* 564* @group Event Related 565*/ 566var MapMovementEvent = class extends MapLibreEvent {}; 567/** 568* The `style.load` event, fired once the map's style has fully loaded or changed. 569* 570* @group Event Related 571*/ 572var MapStyleLoadEvent = class extends MapLibreEvent { 573 constructor(data = {}) { 574 super("style.load", data); 575 } 576}; 577/** 578* The style data event 579* 580* @group Event Related 581*/ 582var MapStyleDataEvent = class extends MapLibreEvent { 583 constructor(type, data = {}) { 584 super(type, data); 585 this.dataType = "style"; 586 } 587}; 588/** 589* A `MapSourceDataEvent` is emitted with the source-related `data`, `dataloading`, `dataabort`, 590* `sourcedata`, `sourcedataloading` and `sourcedataabort` events. Its `dataType` is always `'source'`. 591* 592* Possible values for `sourceDataType`s are: 593* 594* - `'metadata'`: indicates that any necessary source metadata has been loaded (such as TileJSON) and it is ok to start loading tiles 595* - `'content'`: indicates the source data has changed (such as when source.setData() has been called on GeoJSONSource) 596* - `'visibility'`: send when the source becomes used when at least one of its layers becomes visible in style sense (inside the layer's zoom range and with layout.visibility set to 'visible') 597* - `'idle'`: indicates that no new source data has been fetched (but the source has done loading) 598* 599* @group Event Related 600* 601* @example 602* ```ts 603* // The sourcedata event is an example of a MapSourceDataEvent. 604* // Set up an event listener on the map. 605* map.on('sourcedata', (e) => { 606* if (e.isSourceLoaded) { 607* // Do something when the source has finished loading 608* } 609* }); 610* ``` 611*/ 612var MapSourceDataEvent = class extends MapLibreEvent { 613 constructor(type, data = {}) { 614 super(type, data); 615 this.dataType = "source"; 616 } 617}; 618/** 619* `MapMouseEvent` is the event type for mouse-related map events. 620* 621* @group Event Related 622* 623* @example 624* ```ts 625* // The `click` event is an example of a `MapMouseEvent`. 626* // Set up an event listener on the map. 627* map.on('click', (e) => { 628* // The event object (e) contains information like the 629* // coordinates of the point on the map that was clicked. 630* console.log('A click event has occurred at ' + e.lngLat); 631* }); 632* ``` 633*/ 634var MapMouseEvent = class extends MapLibreEvent { 635 /** 636 * Prevents subsequent default processing of the event by the map. 637 * 638 * Calling this method will prevent the following default map behaviors: 639 * 640 * * On `mousedown` events, the behavior of {@link DragPanHandler} 641 * * On `mousedown` events, the behavior of {@link DragRotateHandler} 642 * * On `mousedown` events, the behavior of {@link BoxZoomHandler} 643 * * On `dblclick` events, the behavior of {@link DoubleClickZoomHandler} 644 * 645 */ 646 preventDefault() { 647 this._defaultPrevented = true; 648 } 649 /** 650 * `true` if `preventDefault` has been called. 651 */ 652 get defaultPrevented() { 653 return this._defaultPrevented; 654 } 655 constructor(type, map, originalEvent, data = {}) { 656 originalEvent = originalEvent instanceof MouseEvent ? originalEvent : new MouseEvent(type, originalEvent); 657 const point = DOM.mousePos(map.getCanvas(), originalEvent); 658 const lngLat = map.unproject(point); 659 super(type, extend({ 660 point, 661 lngLat, 662 originalEvent 663 }, data)); 664 this._defaultPrevented = false; 665 this.target = map; 666 } 667}; 668/** 669* `MapTouchEvent` is the event type for touch-related map events. 670* 671* @group Event Related 672*/ 673var MapTouchEvent = class extends MapLibreEvent { 674 /** 675 * Prevents subsequent default processing of the event by the map. 676 * 677 * Calling this method will prevent the following default map behaviors: 678 * 679 * * On `touchstart` events, the behavior of {@link DragPanHandler} 680 * * On `touchstart` events, the behavior of {@link TwoFingersTouchZoomRotateHandler} 681 * 682 */ 683 preventDefault() { 684 this._defaultPrevented = true; 685 } 686 /** 687 * `true` if `preventDefault` has been called. 688 */ 689 get defaultPrevented() { 690 return this._defaultPrevented; 691 } 692 constructor(type, map, originalEvent) { 693 const touches = type === "touchend" ? originalEvent.changedTouches : originalEvent.touches; 694 const points = DOM.touchPos(map.getCanvasContainer(), touches); 695 const lngLats = points.map((t) => map.unproject(t)); 696 const point = points.reduce((prev, curr, i, arr) => { 697 return prev.add(curr.div(arr.length)); 698 }, new Point(0, 0)); 699 const lngLat = map.unproject(point); 700 super(type, { 701 points, 702 point, 703 lngLats, 704 lngLat, 705 originalEvent 706 }); 707 this._defaultPrevented = false; 708 } 709}; 710/** 711* `MapWheelEvent` is the event type for the `wheel` map event. 712* 713* @group Event Related 714*/ 715var MapWheelEvent = class extends MapLibreEvent { 716 /** 717 * Prevents subsequent default processing of the event by the map. 718 * 719 * Calling this method will prevent the behavior of {@link ScrollZoomHandler}. 720 */ 721 preventDefault() { 722 this._defaultPrevented = true; 723 } 724 /** 725 * `true` if `preventDefault` has been called. 726 */ 727 get defaultPrevented() { 728 return this._defaultPrevented; 729 } 730 /** */ 731 constructor(map, originalEvent) { 732 super("wheel", { originalEvent }); 733 this._defaultPrevented = false; 734 } 735}; 736/** 737* A `MapBoxZoomEvent` is the event type for the boxzoom-related map events emitted by the {@link BoxZoomHandler}. 738* 739* @group Event Related 740*/ 741var MapBoxZoomEvent = class extends MapLibreEvent {}; 742/** 743* The terrain event 744* 745* @group Event Related 746*/ 747var MapTerrainEvent = class extends MapLibreEvent { 748 constructor(data = {}) { 749 super("terrain", data); 750 } 751}; 752/** 753* The map projection event 754* 755* @group Event Related 756*/ 757var MapProjectionEvent = class extends MapLibreEvent { 758 constructor(data = {}) { 759 super("projectiontransition", data); 760 } 761}; 762/** 763* An event related to the web gl context 764* 765* @group Event Related 766*/ 767var MapContextEvent = class extends MapLibreEvent {}; 768/** 769* The style image missing event, fired when an image is still missing after the missing style image 770* resolver has been given a chance to supply it. To load or generate images on demand, 771* use {@link Map.setMissingStyleImageResolver}. 772* Event listeners cannot resolve the missing image for the current request. 773* 774* @group Event Related 775* 776* @see [Generate and add a missing icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/generate-and-add-a-missing-icon-to-the-map/) 777*/ 778var MapStyleImageMissingEvent = class extends MapLibreEvent { 779 constructor(data = {}) { 780 super("styleimagemissing", data); 781 } 782}; 783//#endregion 784//#region src/util/style.ts 785/** 786* Takes a SpriteSpecification value and returns it in its array form. If `undefined` is passed as an input value, an 787* empty array is returned. 788* duplicated entries with identical id/url will be removed in returned array 789* @param sprite - optional sprite to coerce 790* @returns an empty array in case `undefined` is passed; id-url pairs otherwise 791*/ 792function coerceSpriteToArray(sprite) { 793 const resultArray = []; 794 if (typeof sprite === "string") resultArray.push({ 795 id: "default", 796 url: sprite 797 }); 798 else if (sprite && sprite.length > 0) { 799 const dedupArray = []; 800 for (const { id, url } of sprite) { 801 const key = `${id}${url}`; 802 if (!dedupArray.includes(key)) { 803 dedupArray.push(key); 804 resultArray.push({ 805 id, 806 url 807 }); 808 } 809 } 810 } 811 return resultArray; 812} 813//#endregion 814//#region src/style/load_sprite.ts 815function normalizeSpriteURL(url, format, extension) { 816 try { 817 const parsed = new URL(url); 818 parsed.pathname += `${format}${extension}`; 819 return parsed.toString(); 820 } catch { 821 throw new Error(`Invalid sprite URL "${url}", must be absolute. Modify style specification directly or use TransformStyleFunction to correct the issue dynamically`); 822 } 823} 824async function loadSprite(originalSprite, requestManager, pixelRatio, abortController) { 825 const spriteArray = coerceSpriteToArray(originalSprite); 826 const format = pixelRatio > 1 ? "@2x" : ""; 827 const jsonsMap = {}; 828 const imagesMap = {}; 829 for (const { id, url } of spriteArray) { 830 const jsonRequestParameters = await requestManager.transformRequest(normalizeSpriteURL(url, format, ".json"), "SpriteJSON"); 831 jsonsMap[id] = getJSON(jsonRequestParameters, abortController); 832 const imageRequestParameters = await requestManager.transformRequest(normalizeSpriteURL(url, format, ".png"), "SpriteImage"); 833 imagesMap[id] = ImageRequest.getImage(imageRequestParameters, abortController); 834 } 835 await Promise.all([...Object.values(jsonsMap), ...Object.values(imagesMap)]); 836 return doOnceCompleted(jsonsMap, imagesMap); 837} 838/** 839* @param jsonsMap - JSON data map 840* @param imagesMap - image data map 841*/ 842async function doOnceCompleted(jsonsMap, imagesMap) { 843 const result = {}; 844 for (const spriteName in jsonsMap) { 845 result[spriteName] = {}; 846 const image = (await imagesMap[spriteName]).data; 847 if (!image) throw new Error(`Could not load sprite image for ${spriteName}: the response is empty`); 848 const context = browser.getImageCanvasContext(image); 849 const json = (await jsonsMap[spriteName]).data; 850 for (const id in json) { 851 const { width, height, x, y, sdf, pixelRatio, stretchX, stretchY, content, textFitWidth, textFitHeight } = json[id]; 852 const spriteData = { 853 width, 854 height, 855 x, 856 y, 857 context 858 }; 859 result[spriteName][id] = { 860 data: null, 861 pixelRatio, 862 sdf, 863 stretchX, 864 stretchY, 865 content, 866 textFitWidth, 867 textFitHeight, 868 spriteData 869 }; 870 } 871 } 872 return result; 873} 874//#endregion 875//#region src/render/image_manager.ts 876/** 877* Owns every image of the style - the ones the sprites bring in as well as the ones added at 878* runtime - and tracks requests for them from the tile workers, sending responses when the 879* requests are fulfilled. 880*/ 881var ImageManager = class extends Evented { 882 constructor() { 883 super(); 884 this.images = {}; 885 this.updateVersion = 0; 886 this.loaded = false; 887 this.requestors = []; 888 this.missingImageResolver = null; 889 this._spriteImagesIds = {}; 890 this._imagesIds = null; 891 this._renderCallbacksDispatchedThisFrame = {}; 892 } 893 destroy() { 894 for (const id of Object.keys(this.images)) this.removeImage(id); 895 this._spriteImagesIds = {}; 896 } 897 isLoaded() { 898 return this.loaded; 899 } 900 setLoaded(loaded) { 901 if (this.loaded === loaded) return; 902 this.loaded = loaded; 903 if (loaded) { 904 for (const { ids, promiseResolve } of this.requestors) promiseResolve(this._getImagesForIds(ids)); 905 this.requestors = []; 906 } 907 } 908 getImage(id) { 909 const image = this.images[id]; 910 if (image && !image.data && image.spriteData) { 911 const spriteData = image.spriteData; 912 image.data = new RGBAImage({ 913 width: spriteData.width, 914 height: spriteData.height 915 }, spriteData.context.getImageData(spriteData.x, spriteData.y, spriteData.width, spriteData.height).data); 916 image.spriteData = null; 917 } 918 return image; 919 } 920 addImage(id, image) { 921 if (this.images[id]) throw new Error(`Image id ${id} already exist, use updateImage instead`); 922 if (this._validate(id, image)) { 923 this.images[id] = image; 924 this._imagesIds = null; 925 if (image.isWebGLImage) this.updateImage(id, image, false); 926 } 927 } 928 _validate(id, image) { 929 let valid = true; 930 const data = image.data || image.spriteData; 931 if (!this._validateStretch(image.stretchX, data?.width)) { 932 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "stretchX" value`))); 933 valid = false; 934 } 935 if (!this._validateStretch(image.stretchY, data?.height)) { 936 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "stretchY" value`))); 937 valid = false; 938 } 939 if (!this._validateContent(image.content, image)) { 940 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "content" value`))); 941 valid = false; 942 } 943 return valid; 944 } 945 _validateStretch(stretch, size) { 946 if (!stretch) return true; 947 let last = 0; 948 for (const part of stretch) { 949 if (part[0] < last || part[1] < part[0] || size < part[1]) return false; 950 last = part[1]; 951 } 952 return true; 953 } 954 _validateContent(content, image) { 955 if (!content) return true; 956 if (content.length !== 4) return false; 957 const spriteData = image.spriteData; 958 const width = spriteData?.width || image.data.width; 959 const height = spriteData?.height || image.data.height; 960 if (content[0] < 0 || width < content[0]) return false; 961 if (content[1] < 0 || height < content[1]) return false; 962 if (content[2] < 0 || width < content[2]) return false; 963 if (content[3] < 0 || height < content[3]) return false; 964 if (content[2] < content[0]) return false; 965 return content[3] >= content[1]; 966 } 967 /** 968 * Replaces an image that is already known under this id, bumping its version so that the 969 * atlases holding it notice. 970 * 971 * The old record is read directly rather than through {@link ImageManager.getImage}, which 972 * would force a synchronous decode of the sprite data of the very image being replaced. Its 973 * size therefore comes from whichever of the two the pixels are still in, since an image that 974 * came from a sprite and was never rendered has not been decoded yet, and its version is 975 * defaulted, since images start out without one. 976 * 977 * @param validate - whether to reject an image of a different size than the one it replaces 978 */ 979 updateImage(id, image, validate = true) { 980 const oldImage = this.images[id]; 981 if (validate) { 982 const oldData = oldImage.data || oldImage.spriteData; 983 if (oldData.width !== image.data.width || oldData.height !== image.data.height) throw new Error(`size mismatch between old image (${oldData.width}x${oldData.height}) and new image (${image.data.width}x${image.data.height}).`); 984 } 985 image.version = (oldImage.version ?? 0) + 1; 986 this.images[id] = image; 987 this.updateVersion++; 988 } 989 removeImage(id) { 990 const image = this.images[id]; 991 if (!image) return; 992 delete this.images[id]; 993 this._imagesIds = null; 994 if (image.userImage?.onRemove) image.userImage.onRemove(); 995 } 996 /** 997 * @returns the ids of every image currently held, sprite images and runtime ones alike. The 998 * returned array is shared between callers and must not be modified. 999 */ 1000 listImages() { 1001 this._imagesIds ??= Object.keys(this.images); 1002 return this._imagesIds; 1003 } 1004 /** 1005 * Images of the `default` sprite keep their plain id, the ones of any other sprite are 1006 * namespaced by their sprite's id. 1007 */ 1008 _getSpriteImageId(spriteId, imageId) { 1009 return spriteId === "default" ? imageId : `${spriteId}:${imageId}`; 1010 } 1011 /** 1012 * Takes over the images of a single sprite: adds the new ones, updates the ones that are 1013 * already known, and removes the ones that a previous load of this same sprite had brought in 1014 * but that are not part of it anymore. 1015 * 1016 * @param spriteId - the id of the sprite the images belong to 1017 * @param images - the sprite's images, keyed by their id within the sprite 1018 * @returns the ids of the images that this sprite now provides, and the ids of the ones that 1019 * were removed because they are no longer part of it 1020 */ 1021 setSpriteImages(spriteId, images) { 1022 const previousIds = this._spriteImagesIds[spriteId] ?? []; 1023 const loaded = []; 1024 for (const id in images) { 1025 const imageId = this._getSpriteImageId(spriteId, id); 1026 loaded.push(imageId); 1027 if (imageId in this.images) this.updateImage(imageId, images[id], false); 1028 else this.addImage(imageId, images[id]); 1029 } 1030 const loadedIds = new Set(loaded); 1031 const removed = previousIds.filter((imageId) => !loadedIds.has(imageId)); 1032 for (const imageId of removed) this.removeImage(imageId); 1033 this._spriteImagesIds[spriteId] = loaded; 1034 return { 1035 loaded, 1036 removed 1037 }; 1038 } 1039 /** 1040 * Removes all the images a single sprite has brought in. 1041 * @returns the ids of the removed images 1042 */ 1043 removeSpriteImages(spriteId) { 1044 const removed = this._spriteImagesIds[spriteId] ?? []; 1045 for (const imageId of removed) this.removeImage(imageId); 1046 delete this._spriteImagesIds[spriteId]; 1047 return removed; 1048 } 1049 /** 1050 * Removes the images of every sprite, leaving the images added at runtime alone. 1051 * @returns the ids of the removed images 1052 */ 1053 removeAllSpriteImages() { 1054 const removed = Object.values(this._spriteImagesIds).flat(); 1055 for (const imageId of removed) this.removeImage(imageId); 1056 this._spriteImagesIds = {}; 1057 return removed; 1058 } 1059 setMissingImageResolver(resolver) { 1060 this.missingImageResolver = resolver; 1061 } 1062 getImages(ids) { 1063 return new Promise((resolve, _reject) => { 1064 let hasAllDependencies = true; 1065 if (!this.isLoaded()) { 1066 for (const id of ids) if (!this.images[id]) hasAllDependencies = false; 1067 } 1068 if (this.isLoaded() || hasAllDependencies) resolve(this._getImagesForIds(ids)); 1069 else this.requestors.push({ 1070 ids, 1071 promiseResolve: resolve 1072 }); 1073 }); 1074 } 1075 async _getImagesForIds(ids) { 1076 const unresolvedIds = new Set(ids.filter((id) => !this.getImage(id))); 1077 const resolver = this.missingImageResolver; 1078 if (resolver) await Promise.allSettled(Array.from(unresolvedIds, (id) => resolver(id))); 1079 const response = {}; 1080 for (const id of ids) { 1081 const image = this.getImage(id); 1082 if (image) { 1083 unresolvedIds.delete(id); 1084 response[id] = { 1085 data: image.data.clone(), 1086 pixelRatio: image.pixelRatio, 1087 sdf: image.sdf, 1088 version: image.version, 1089 stretchX: image.stretchX, 1090 stretchY: image.stretchY, 1091 content: image.content, 1092 textFitWidth: image.textFitWidth, 1093 textFitHeight: image.textFitHeight, 1094 hasRenderCallback: Boolean(image.userImage?.render), 1095 isWebGLImage: image.isWebGLImage 1096 }; 1097 } 1098 } 1099 for (const id of unresolvedIds) { 1100 this.fire(new MapStyleImageMissingEvent({ id })); 1101 warnOnce(`Image "${id}" could not be loaded. Please make sure you have added the image before it is needed with map.addImage(), resolved it with map.setMissingStyleImageResolver(), or included it in a "sprite" property in your style.`); 1102 } 1103 return response; 1104 } 1105 beginFrame() { 1106 this._renderCallbacksDispatchedThisFrame = {}; 1107 } 1108 /** 1109 * Re-renders the images among `ids` that were added with a `render` callback (see 1110 * `StyleImageInterface`), at most once per frame each. 1111 */ 1112 dispatchRenderCallbacks(ids) { 1113 for (const id of ids) { 1114 if (this._renderCallbacksDispatchedThisFrame[id]) continue; 1115 this._renderCallbacksDispatchedThisFrame[id] = true; 1116 const image = this.getImage(id); 1117 if (!image) warnOnce(`Image with ID: "${id}" was not found`); 1118 if (renderStyleImage(image)) this.updateImage(id, image); 1119 } 1120 } 1121 cloneImages() { 1122 const clonedImages = {}; 1123 for (const id in this.images) { 1124 const image = this.images[id]; 1125 clonedImages[id] = { 1126 ...image, 1127 data: image.data ? image.data.clone() : null 1128 }; 1129 } 1130 return clonedImages; 1131 } 1132}; 1133//#endregion 1134//#region src/render/pattern_atlas.ts 1135/** 1136* When copied into the atlas texture, pattern images are padded by one pixel on each side with a 1137* copy of the image data wrapped from the opposite side. This ensures the correct behavior of 1138* GL_LINEAR texture sampling mode. 1139*/ 1140const PADDING = 1; 1141/** 1142* A texture atlas of the pattern images that are drawn without a tile's own {@link ImageAtlas}, 1143* i.e. `background-pattern`. Entries are packed on demand and re-packed whenever one of them is 1144* added or changes, which is rare enough for the repacking cost not to matter. 1145*/ 1146var PatternAtlas = class { 1147 constructor(imageManager) { 1148 this._imageManager = imageManager; 1149 this._entries = {}; 1150 this._image = new RGBAImage({ 1151 width: 1, 1152 height: 1 1153 }); 1154 this._dirty = true; 1155 } 1156 destroy() { 1157 if (this._texture) { 1158 this._texture.destroy(); 1159 this._texture = null; 1160 } 1161 this._entries = {}; 1162 this._image = new RGBAImage({ 1163 width: 1, 1164 height: 1 1165 }); 1166 this._dirty = true; 1167 } 1168 getPixelSize() { 1169 const { width, height } = this._image; 1170 return { 1171 width, 1172 height 1173 }; 1174 } 1175 /** 1176 * @returns the position of the pattern within the atlas, or `null` if there is no such image 1177 */ 1178 getPattern(id) { 1179 const image = this._imageManager.getImage(id); 1180 if (!image) return null; 1181 const entry = this._entries[id]; 1182 if (entry?.image !== image) { 1183 const bin = { 1184 w: image.data.width + 2, 1185 h: image.data.height + 2, 1186 x: 0, 1187 y: 0 1188 }; 1189 this._entries[id] = { 1190 bin, 1191 position: new ImagePosition(bin, image), 1192 image 1193 }; 1194 } else if (entry.position.version !== image.version) entry.position.version = image.version; 1195 else return entry.position; 1196 this._update(); 1197 return this._entries[id].position; 1198 } 1199 bind(context) { 1200 const gl = context.gl; 1201 if (!this._texture) { 1202 this._texture = new Texture(context, this._image, gl.RGBA); 1203 this._dirty = false; 1204 } else if (this._dirty) { 1205 this._texture.update(this._image); 1206 this._dirty = false; 1207 } 1208 this._texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 1209 } 1210 _update() { 1211 for (const id in this._entries) if (!this._imageManager.getImage(id)) delete this._entries[id]; 1212 const bins = []; 1213 for (const id in this._entries) bins.push(this._entries[id].bin); 1214 const { w, h } = potpack(bins); 1215 const dst = this._image; 1216 dst.resize({ 1217 width: w || 1, 1218 height: h || 1 1219 }); 1220 for (const id in this._entries) { 1221 const { bin } = this._entries[id]; 1222 const x = bin.x + PADDING; 1223 const y = bin.y + PADDING; 1224 const src = this._entries[id].image.data; 1225 const w = src.width; 1226 const h = src.height; 1227 RGBAImage.copy(src, dst, { 1228 x: 0, 1229 y: 0 1230 }, { 1231 x, 1232 y 1233 }, { 1234 width: w, 1235 height: h 1236 }); 1237 RGBAImage.copy(src, dst, { 1238 x: 0, 1239 y: h - 1 1240 }, { 1241 x, 1242 y: y - 1 1243 }, { 1244 width: w, 1245 height: 1 1246 }); 1247 RGBAImage.copy(src, dst, { 1248 x: 0, 1249 y: 0 1250 }, { 1251 x, 1252 y: y + h 1253 }, { 1254 width: w, 1255 height: 1 1256 }); 1257 RGBAImage.copy(src, dst, { 1258 x: w - 1, 1259 y: 0 1260 }, { 1261 x: x - 1, 1262 y 1263 }, { 1264 width: 1, 1265 height: h 1266 }); 1267 RGBAImage.copy(src, dst, { 1268 x: 0, 1269 y: 0 1270 }, { 1271 x: x + w, 1272 y 1273 }, { 1274 width: 1, 1275 height: h 1276 }); 1277 } 1278 this._dirty = true; 1279 } 1280}; 1281//#endregion 1282//#region src/render/font_face_manager.ts 1283const MAX_CODE_POINT = 1114111; 1284/** 1285* What a font file covers when the style does not narrow it down with `unicode-range`. 1286*/ 1287const DEFAULT_UNICODE_RANGE = { 1288 start: 0, 1289 end: MAX_CODE_POINT 1290}; 1291/** 1292* Makes the family names unique across every map on the page, since `document.fonts` is shared. 1293*/ 1294let nextFamilyId = 0; 1295/** 1296* Parses one `unicode-range` entry, in the CSS grammar the style specification borrows: `U+A5`, 1297* `U+0-10FFFF` or `U+4??`. 1298* @param value - a single `unicode-range` entry 1299* @returns the codepoints it covers, or `null` if it is not a range this can make sense of 1300*/ 1301function parseUnicodeRange(value) { 1302 const wildcard = /^u\+([0-9a-f]*)(\?+)$/i.exec(value); 1303 if (wildcard) { 1304 const [, prefix, questionMarks] = wildcard; 1305 if (prefix.length + questionMarks.length > 6) return null; 1306 return clampRange(parseInt(`${prefix}${"0".repeat(questionMarks.length)}`, 16), parseInt(`${prefix}${"f".repeat(questionMarks.length)}`, 16)); 1307 } 1308 const explicit = /^u\+([0-9a-f]{1,6})(?:-([0-9a-f]{1,6}))?$/i.exec(value); 1309 if (!explicit) return null; 1310 const start = parseInt(explicit[1], 16); 1311 return clampRange(start, explicit[2] === void 0 ? start : parseInt(explicit[2], 16)); 1312} 1313function clampRange(start, end) { 1314 if (start > end || start > MAX_CODE_POINT) return null; 1315 return { 1316 start, 1317 end: Math.min(end, MAX_CODE_POINT) 1318 }; 1319} 1320/** 1321* Whether the style lets this file cover the given codepoint. 1322*/ 1323function covers(face, codePoint) { 1324 return face.ranges.some(({ start, end }) => codePoint >= start && codePoint <= end); 1325} 1326/** 1327* Keeps the font files a style declares in its 1328* [`font-faces`](https://maplibre.org/maplibre-style-spec/root/#font-faces) property, and hands the 1329* {@link GlyphManager} the CSS family to draw a given codepoint with. 1330* 1331* The files go to the browser's CSS Font Loading API, so any format it can render text with works. 1332* Each is registered under a family name of our own making, so that a style cannot restyle the 1333* surrounding page and a codepoint can be pinned to one file rather than to whatever font matching 1334* picks. 1335* 1336* Rasterizing happens a grapheme cluster at a time, so a letter reaches the browser's text engine 1337* with its marks. Shaping across cluster boundaries still needs the RTL text plugin. 1338*/ 1339var FontFaceManager = class { 1340 constructor(requestManager) { 1341 this.requestManager = requestManager; 1342 this._faces = {}; 1343 this._registered = /* @__PURE__ */ new Set(); 1344 } 1345 /** 1346 * Replaces the declared font faces. Nothing is downloaded here: each file waits until a 1347 * codepoint it covers is actually drawn. 1348 */ 1349 setFontFaces(fontFaces) { 1350 this._unregisterAll(); 1351 this._faces = {}; 1352 for (const [fontName, declaration] of Object.entries(fontFaces ?? {})) { 1353 const declarations = Array.isArray(declaration) ? declaration : [declaration]; 1354 this._faces[fontName] = declarations.map((entry) => this._declareFontFace(fontName, entry)).filter((face) => face !== null); 1355 } 1356 } 1357 /** 1358 * Whether the style declared any font face at all, so that the common case of a style without 1359 * `font-faces` costs nothing. 1360 */ 1361 hasFontFaces() { 1362 return Object.keys(this._faces).length > 0; 1363 } 1364 /** 1365 * Finds the font file to draw a codepoint with: each name of the `text-font` stack in turn, and 1366 * within a name each declared file, until one covers it. A file that fails to load is skipped, 1367 * the specification asking for unsupported fonts to be ignored. 1368 * 1369 * @returns the CSS family to draw with, or `null` to leave the codepoint to the `glyphs` URL 1370 */ 1371 async getFontFamily(fontStack, codePoint) { 1372 for (const fontName of fontStack.split(",")) for (const face of this._faces[fontName.trim()] ?? []) { 1373 if (!covers(face, codePoint)) continue; 1374 face.loaded ??= this._loadFontFace(face); 1375 if (await face.loaded) return face.family; 1376 } 1377 return null; 1378 } 1379 /** 1380 * Turns one declaration into a face to draw with, or `null` if there is nothing usable in it. 1381 * 1382 * A bare URL is the same as a face that names no `unicode-range`: it covers every codepoint. 1383 */ 1384 _declareFontFace(fontName, declaration) { 1385 const face = typeof declaration === "string" ? { url: declaration } : declaration; 1386 if (typeof face?.url !== "string") { 1387 warnOnce(`Ignoring the font face declared for "${fontName}": it has no URL.`); 1388 return null; 1389 } 1390 const family = `maplibre-gl-font-face-${nextFamilyId++}`; 1391 const unicodeRange = face["unicode-range"]; 1392 if (!unicodeRange?.length) return { 1393 url: face.url, 1394 ranges: [DEFAULT_UNICODE_RANGE], 1395 family 1396 }; 1397 const ranges = []; 1398 for (const entry of unicodeRange) { 1399 const range = parseUnicodeRange(entry); 1400 if (!range) { 1401 warnOnce(`Ignoring the unicode range "${entry}" of the font face at ${face.url}: it is not a valid range.`); 1402 continue; 1403 } 1404 ranges.push(range); 1405 } 1406 if (!ranges.length) return null; 1407 return { 1408 url: face.url, 1409 ranges, 1410 family 1411 }; 1412 } 1413 /** 1414 * Downloads a declared file and hands it to the browser, reporting whether it can be drawn with. 1415 * A failure is not an error: its codepoints fall through to the next file, then to `glyphs`. 1416 */ 1417 async _loadFontFace(face) { 1418 if (typeof FontFace === "undefined" || typeof document === "undefined" || !document.fonts) { 1419 warnOnce(`Ignoring the font face at ${face.url}: this environment has no CSS Font Loading API.`); 1420 return false; 1421 } 1422 let fontFace; 1423 try { 1424 fontFace = new FontFace(face.family, await this._downloadFontFile(face.url)); 1425 if (!Object.values(this._faces).some((faces) => faces.includes(face))) return false; 1426 document.fonts.add(fontFace); 1427 this._registered.add(fontFace); 1428 await fontFace.load(); 1429 return true; 1430 } catch (e) { 1431 if (fontFace) this._unregister(fontFace); 1432 warnOnce(`Ignoring the font face at ${face.url}: ${ensureError(e).message}`); 1433 return false; 1434 } 1435 } 1436 /** 1437 * Downloads a font file, giving the map's `transformRequest` a chance to rewrite the request the 1438 * same way it gets one for a glyph range. 1439 */ 1440 async _downloadFontFile(url) { 1441 const request = await this.requestManager.transformRequest(url, "Glyphs"); 1442 const response = await getArrayBuffer(request, new AbortController()); 1443 if (!response?.data) throw new Error(`the response was empty for the font file at ${url}`); 1444 return response.data; 1445 } 1446 _unregister(fontFace) { 1447 document.fonts?.delete(fontFace); 1448 this._registered.delete(fontFace); 1449 } 1450 _unregisterAll() { 1451 for (const fontFace of this._registered) document.fonts?.delete(fontFace); 1452 this._registered.clear(); 1453 } 1454 destroy() { 1455 this._unregisterAll(); 1456 this._faces = {}; 1457 } 1458}; 1459//#endregion 1460//#region node_modules/@mapbox/tiny-sdf/index.js 1461const INF = 0x56bc75e2d63100000; 1462const alphaTable = /* @__PURE__ */ new Float64Array(256); 1463for (let i = 0; i < 256; i++) { 1464 const d = .5 - Math.pow(i / 255, 1 / 2.2); 1465 alphaTable[i] = d * Math.abs(d); 1466} 1467alphaTable[255] = -0x56bc75e2d63100000; 1468var TinySDF = class { 1469 constructor({ fontSize = 24, buffer = 3, radius = 8, cutoff = .25, fontFamily = "sans-serif", fontWeight = "normal", fontStyle = "normal", lang = null } = {}) { 1470 this.buffer = buffer; 1471 this.radius = radius; 1472 this.cutoff = cutoff; 1473 this.lang = lang; 1474 const size = this.size = fontSize + buffer * 4; 1475 const canvas = this._createCanvas(size); 1476 const ctx = this.ctx = canvas.getContext("2d", { willReadFrequently: true }); 1477 ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; 1478 ctx.textBaseline = "alphabetic"; 1479 ctx.textAlign = "left"; 1480 ctx.fillStyle = "black"; 1481 this.gridOuter = new Float64Array(size * size); 1482 this.gridInner = new Float64Array(size * size); 1483 this.f = new Float64Array(size); 1484 this.z = new Float64Array(size + 1); 1485 this.v = new Uint16Array(size); 1486 } 1487 _createCanvas(size) { 1488 if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(size, size); 1489 const canvas = document.createElement("canvas"); 1490 canvas.width = canvas.height = size; 1491 return canvas; 1492 } 1493 draw(char) { 1494 const { width: glyphAdvance, actualBoundingBoxAscent, actualBoundingBoxDescent, actualBoundingBoxLeft, actualBoundingBoxRight } = this.ctx.measureText(char); 1495 const glyphTop = Math.ceil(actualBoundingBoxAscent); 1496 const glyphLeft = Math.floor(-actualBoundingBoxLeft); 1497 const glyphWidth = Math.max(0, Math.min(this.size - this.buffer, Math.ceil(actualBoundingBoxRight) - glyphLeft)); 1498 const glyphHeight = Math.max(0, Math.min(this.size - this.buffer, glyphTop + Math.ceil(actualBoundingBoxDescent))); 1499 const width = glyphWidth + 2 * this.buffer; 1500 const height = glyphHeight + 2 * this.buffer; 1501 const len = Math.max(width * height, 0); 1502 const data = new Uint8ClampedArray(len); 1503 const glyph = { 1504 data, 1505 width, 1506 height, 1507 glyphWidth, 1508 glyphHeight, 1509 glyphTop, 1510 glyphLeft, 1511 glyphAdvance 1512 }; 1513 if (glyphWidth === 0 || glyphHeight === 0) return glyph; 1514 const { ctx, buffer, gridInner, gridOuter } = this; 1515 if (this.lang) ctx.lang = this.lang; 1516 ctx.clearRect(buffer, buffer, glyphWidth, glyphHeight); 1517 ctx.fillText(char, buffer - glyphLeft, buffer + glyphTop); 1518 const imgData = ctx.getImageData(buffer, buffer, glyphWidth, glyphHeight); 1519 gridOuter.fill(INF, 0, len); 1520 gridInner.fill(0, 0, len); 1521 let imgIdx = 3; 1522 for (let y = 0; y < glyphHeight; y++) { 1523 let j = (y + buffer) * width + buffer; 1524 for (let x = 0; x < glyphWidth; x++, imgIdx += 4, j++) { 1525 const a = imgData.data[imgIdx]; 1526 if (a === 0) continue; 1527 const t = alphaTable[a]; 1528 gridOuter[j] = Math.max(0, t); 1529 gridInner[j] = Math.max(0, -t); 1530 } 1531 } 1532 edt(gridOuter, 0, 0, width, height, width, this.f, this.v, this.z); 1533 const pad = Math.min(buffer, 1); 1534 edt(gridInner, buffer - pad, buffer - pad, glyphWidth + 2 * pad, glyphHeight + 2 * pad, width, this.f, this.v, this.z); 1535 const scale = 255 / this.radius; 1536 const base = 255 * (1 - this.cutoff); 1537 for (let i = 0; i < len; i++) { 1538 const d = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]); 1539 data[i] = Math.round(base - scale * d); 1540 } 1541 return glyph; 1542 } 1543}; 1544function edt(data, x0, y0, width, height, gridSize, f, v, z) { 1545 for (let x = x0; x < x0 + width; x++) edt1d(data, y0 * gridSize + x, gridSize, height, f, v, z); 1546 for (let y = y0; y < y0 + height; y++) edt1d(data, y * gridSize + x0, 1, width, f, v, z); 1547} 1548function edt1d(grid, offset, stride, length, f, v, z) { 1549 v[0] = 0; 1550 z[0] = -0x56bc75e2d63100000; 1551 z[1] = INF; 1552 f[0] = grid[offset]; 1553 for (let q = 1, k = 0, s = 0; q < length; q++) { 1554 f[q] = grid[offset + q * stride]; 1555 const q2 = q * q; 1556 do { 1557 const r = v[k]; 1558 s = (f[q] - f[r] + q2 - r * r) / (q - r) / 2; 1559 } while (s <= z[k] && --k > -1); 1560 k++; 1561 v[k] = q; 1562 z[k] = s; 1563 z[k + 1] = INF; 1564 } 1565 for (let q = 0, k = 0; q < length; q++) { 1566 while (z[k + 1] < q) k++; 1567 const r = v[k]; 1568 const qr = q - r; 1569 grid[offset + q * stride] = f[r] + qr * qr; 1570 } 1571} 1572//#endregion 1573//#region src/render/glyph_manager.ts 1574/** 1575* The style specification hard-codes some last resort fonts as a default fontstack. 1576*/ 1577const defaultStack = latest.layout_symbol["text-font"].default.join(","); 1578/** 1579* The CSS generic font family closest to `defaultStack`. 1580*/ 1581const defaultGenericFontFamily = "sans-serif"; 1582/** 1583* Scale factor for client-generated glyphs. 1584* 1585* Client-generated glyphs are rendered at 2à because CJK glyphs are more detailed than others. 1586*/ 1587const textureScale = 2; 1588const defaultCreateRasterizer = (options, padding) => { 1589 const tinySDF = new TinySDF(options); 1590 tinySDF.buffer = padding; 1591 return tinySDF; 1592}; 1593/** 1594* How wide a grapheme cluster may be drawn, in multiples of the font size. 1595* 1596* TinySDF sizes its canvas for a single character and cuts off the rest, but a cluster is a whole 1597* syllable: Burmese `áá¬á¸` is nearly twice the font size. Three ems fits the ones that occur. 1598*/ 1599const clusterEmsWide = 3; 1600var GlyphManager = class { 1601 constructor(requestManager, localIdeographFontFamily, lang, createRasterizer = defaultCreateRasterizer) { 1602 this.requestManager = requestManager; 1603 this.localIdeographFontFamily = localIdeographFontFamily; 1604 this.entries = {}; 1605 this.lang = lang; 1606 this.fontFaceManager = new FontFaceManager(requestManager); 1607 this.createRasterizer = createRasterizer; 1608 } 1609 setURL(url) { 1610 this.url = url; 1611 } 1612 /** 1613 * Replaces the font files the style declares in its `font-faces` property, dropping every glyph 1614 * drawn with the previous ones. 1615 */ 1616 setFontFaces(fontFaces) { 1617 this.fontFaceManager.setFontFaces(fontFaces); 1618 this.entries = {}; 1619 } 1620 async getGlyphs(glyphs) { 1621 const glyphsPromises = []; 1622 for (const stack in glyphs) for (const id of glyphs[stack]) glyphsPromises.push(this._getAndCacheGlyphsPromise(stack, id)); 1623 const updatedGlyphs = await Promise.all(glyphsPromises); 1624 const result = {}; 1625 for (const { stack, id, glyph } of updatedGlyphs) { 1626 result[stack] ||= {}; 1627 result[stack][id] = glyph && { 1628 id: glyph.id, 1629 bitmap: glyph.bitmap.clone(), 1630 metrics: glyph.metrics 1631 }; 1632 } 1633 return result; 1634 } 1635 /** 1636 * Gets one glyph, asked for by grapheme cluster so that a letter and its marks are drawn as the 1637 * one shape they are written as. 1638 * 1639 * A cluster is drawn from a font rather than fetched, because a glyphs URL serves codepoints and 1640 * has no way to serve the one shape they are written as. A file the style pinned with 1641 * `font-faces` draws it where one covers it, and the local fonts otherwise. For a single 1642 * codepoint a declared file still wins over the glyphs URL and the local fallbacks. 1643 */ 1644 async _getAndCacheGlyphsPromise(stack, id) { 1645 this.entries[stack] ??= { 1646 glyphs: {}, 1647 requests: {}, 1648 ranges: {} 1649 }; 1650 const entry = this.entries[stack]; 1651 let glyph = entry.glyphs[id]; 1652 if (glyph !== void 0) return { 1653 stack, 1654 id, 1655 glyph 1656 }; 1657 const codePoint = id.codePointAt(0); 1658 const fontFaceFamily = this.fontFaceManager.hasFontFaces() ? await this.fontFaceManager.getFontFamily(stack, codePoint) : null; 1659 if (fontFaceFamily) { 1660 glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id, fontFaceFamily); 1661 return { 1662 stack, 1663 id, 1664 glyph 1665 }; 1666 } 1667 if (!this.url || isCluster(id) || this._charUsesLocalIdeographFontFamily(codePoint)) { 1668 glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id); 1669 return { 1670 stack, 1671 id, 1672 glyph 1673 }; 1674 } 1675 return await this._downloadAndCacheRangePromise(stack, id); 1676 } 1677 /** 1678 * Gets a glyph from the server-side cache, downloading the PBF range it falls in if need be. 1679 * 1680 * Only reached for a single codepoint. What comes back is keyed by codepoint, as the file is, 1681 * and is cached by cluster -- which for one codepoint is the character itself. 1682 */ 1683 async _downloadAndCacheRangePromise(stack, id) { 1684 const codePoint = id.codePointAt(0); 1685 const entry = this.entries[stack]; 1686 const range = Math.floor(codePoint / 256); 1687 if (entry.ranges[range]) return { 1688 stack, 1689 id, 1690 glyph: null 1691 }; 1692 entry.requests[range] ||= this._loadGlyphRange(stack, range); 1693 try { 1694 const response = await entry.requests[range]; 1695 for (const responseId in response) entry.glyphs[String.fromCodePoint(+responseId)] = response[+responseId]; 1696 entry.ranges[range] = true; 1697 return { 1698 stack, 1699 id, 1700 glyph: response[codePoint] || null 1701 }; 1702 } catch (e) { 1703 const glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id); 1704 this._warnOnMissingGlyphRange(glyph, range, codePoint, ensureError(e)); 1705 return { 1706 stack, 1707 id, 1708 glyph 1709 }; 1710 } 1711 } 1712 /** 1713 * Downloads one range of 256 codepoints from the glyphs URL and parses the glyphs out of it. 1714 */ 1715 async _loadGlyphRange(fontstack, range) { 1716 const begin = range * 256; 1717 const end = begin + 255; 1718 const request = await this.requestManager.transformRequest(this.url.replace("{fontstack}", fontstack).replace("{range}", `${begin}-${end}`), "Glyphs"); 1719 const response = await getArrayBuffer(request, new AbortController()); 1720 if (!response?.data) throw new Error(`Could not load glyph range. range: ${range}, ${begin}-${end}`); 1721 const glyphs = {}; 1722 for (const glyph of parseGlyphPbf(response.data)) glyphs[glyph.id] = glyph; 1723 return glyphs; 1724 } 1725 _warnOnMissingGlyphRange(glyph, range, id, err) { 1726 const begin = range * 256; 1727 const end = begin + 255; 1728 const codePoint = id.toString(16).padStart(4, "0").toUpperCase(); 1729 warnOnce(`Unable to load glyph range ${range}, ${begin}-${end}. Rendering codepoint U+${codePoint} locally instead. ${err}`); 1730 } 1731 /** 1732 * Returns whether the given codepoint should be rendered locally. 1733 */ 1734 _charUsesLocalIdeographFontFamily(id) { 1735 return !!this.localIdeographFontFamily && codePointUsesLocalIdeographFontFamily(id); 1736 } 1737 /** 1738 * Draws a glyph offscreen using TinySDF, created lazily. The whole cluster goes to TinySDF, which 1739 * is what lets the browser's text engine place a letter's marks on it. 1740 * 1741 * @param fontFaceFamily - the CSS family of the `font-faces` file covering this codepoint, if any 1742 */ 1743 async _drawGlyph(entry, stack, id, fontFaceFamily) { 1744 const char = (await this._getTinySDF(entry, stack, id, fontFaceFamily)).draw(id); 1745 /** 1746 * TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph. 1747 * Server-generated fonts specify "top" relative to an origin above the em box (the origin 1748 * comes from FreeType, but I'm unclear on exactly how it's derived) 1749 * ref: https://github.com/mapbox/sdf-glyph-foundry 1750 * 1751 * Server fonts don't yet include baseline information, so we can't line up exactly with them 1752 * (and they don't line up with each other) 1753 * ref: https://github.com/mapbox/node-fontnik/pull/160 1754 * 1755 * To approximately align TinySDF glyphs with server-provided glyphs, we use this baseline adjustment 1756 * factor calibrated to be in between DIN Pro and Arial Unicode (but closer to Arial Unicode) 1757 */ 1758 const topAdjustment = 27.5; 1759 const leftAdjustment = .5; 1760 const isControl = /^\p{gc=Cf}+$/u.test(id); 1761 return { 1762 id: id.codePointAt(0), 1763 bitmap: new AlphaImage({ 1764 width: char.width || 60, 1765 height: char.height || 60 1766 }, char.data), 1767 metrics: { 1768 width: isControl ? 0 : char.glyphWidth / textureScale || 24, 1769 height: char.glyphHeight / textureScale || 24, 1770 left: char.glyphLeft / textureScale + leftAdjustment || 0, 1771 top: char.glyphTop / textureScale - topAdjustment || -8, 1772 advance: isControl ? 0 : char.glyphAdvance / textureScale || 24, 1773 isDoubleResolution: true 1774 } 1775 }; 1776 } 1777 /** 1778 * Returns the TinySDF that draws this grapheme, created lazily. A stack keeps one per font 1779 * selection, so a fallback cannot bleed into the rest of the text, and one more per font file 1780 * for the clusters drawn from it, which need a wider canvas. 1781 * 1782 * A font file carries its own weight and style, so neither is sniffed out of the family name. 1783 * Where no file covers the grapheme, `localIdeographFontFamily` beats the last resort fontstack. 1784 */ 1785 _getTinySDF(entry, stack, id, fontFaceFamily) { 1786 const cluster = isCluster(id); 1787 if (fontFaceFamily) { 1788 const cache = cluster ? "clusterTinySDFs" : "fontFaceTinySDFs"; 1789 entry[cache] ??= {}; 1790 entry[cache][fontFaceFamily] ||= this._createTinySDF(fontFaceFamily, false, cluster ? clusterEmsWide : 1); 1791 return entry[cache][fontFaceFamily]; 1792 } 1793 const usesLocalIdeographFontFamily = stack === defaultStack && this.localIdeographFontFamily !== "" && this._charUsesLocalIdeographFontFamily(id.codePointAt(0)); 1794 const family = usesLocalIdeographFontFamily ? this.localIdeographFontFamily : stack; 1795 if (cluster) { 1796 entry.clusterTinySDFs ??= {}; 1797 entry.clusterTinySDFs[family] ||= this._createTinySDF(family, true, clusterEmsWide); 1798 return entry.clusterTinySDFs[family]; 1799 } 1800 const cache = usesLocalIdeographFontFamily ? "ideographTinySDF" : "tinySDF"; 1801 entry[cache] ||= this._createTinySDF(family); 1802 return entry[cache]; 1803 } 1804 /** 1805 * Builds the TinySDF that draws with a given font selection. 1806 * 1807 * TinySDF derives its canvas from `fontSize + buffer * 4`, so the buffer is the only way in to a 1808 * wider one. It also stands for the padding the atlas expects to be `GLYPH_PBF_BORDER`, so the 1809 * two are passed separately. 1810 * 1811 * @param emsWide - how wide the glyphs may be, in font sizes, before TinySDF cuts them off 1812 */ 1813 async _createTinySDF(stack, sniffFontStyles = true, emsWide = 1) { 1814 const fontFamilies = stack ? stack.split(",") : []; 1815 fontFamilies.push(defaultGenericFontFamily); 1816 const fontFamily = fontFamilies.map((fontName) => /[-\w]+/.test(fontName) ? fontName : `'${CSS.escape(fontName)}'`).join(","); 1817 const fontSize = 48; 1818 const fontWeight = sniffFontStyles ? this._fontWeight(fontFamilies[0]) : void 0; 1819 const fontStyle = sniffFontStyles ? this._fontStyle(fontFamilies[0]) : "normal"; 1820 if (typeof document !== "undefined" && document.fonts?.load) try { 1821 await document.fonts.load(`${fontStyle} ${fontWeight || "normal"} ${fontSize}px ${fontFamily}`); 1822 } catch (e) { 1823 warnOnce(`Failed to load font "${fontFamily}": ${ensureError(e).message}`); 1824 } 1825 const padding = 6; 1826 return this.createRasterizer({ 1827 fontSize, 1828 buffer: Math.max(padding, Math.ceil(fontSize * (emsWide - 1) / 4)), 1829 radius: 16, 1830 cutoff: .25, 1831 fontFamily, 1832 fontWeight, 1833 fontStyle, 1834 lang: this.lang 1835 }, padding); 1836 } 1837 /** 1838 * Sniffs the font style out of a font family name. 1839 */ 1840 _fontStyle(fontFamily) { 1841 if (/italic/i.test(fontFamily)) return "italic"; 1842 else if (/oblique/i.test(fontFamily)) return "oblique"; 1843 return "normal"; 1844 } 1845 /** 1846 * Sniffs the font weight out of a font family name. 1847 */ 1848 _fontWeight(fontFamily) { 1849 const weightsByName = { 1850 thin: 100, 1851 hairline: 100, 1852 "extra light": 200, 1853 "ultra light": 200, 1854 light: 300, 1855 normal: 400, 1856 regular: 400, 1857 medium: 500, 1858 semibold: 600, 1859 demibold: 600, 1860 bold: 700, 1861 "extra bold": 800, 1862 "ultra bold": 800, 1863 black: 900, 1864 heavy: 900, 1865 "extra black": 950, 1866 "ultra black": 950 1867 }; 1868 let match; 1869 for (const [name, weight] of Object.entries(weightsByName)) if (new RegExp(`\\b${name}\\b`, "i").test(fontFamily)) match = `${weight}`; 1870 return match; 1871 } 1872 destroy() { 1873 for (const stack in this.entries) { 1874 const entry = this.entries[stack]; 1875 entry.tinySDF = null; 1876 entry.ideographTinySDF = null; 1877 entry.fontFaceTinySDFs = {}; 1878 entry.glyphs = {}; 1879 entry.requests = {}; 1880 entry.ranges = {}; 1881 } 1882 this.entries = {}; 1883 this.fontFaceManager.destroy(); 1884 } 1885}; 1886//#endregion 1887//#region src/style/light_properties.g.ts 1888let properties$2; 1889const getProperties$2 = () => properties$2 = properties$2 || new Properties({ 1890 "anchor": new DataConstantProperty(latest["light"]["anchor"], "anchor"), 1891 "position": new DataConstantProperty(latest["light"]["position"], "position"), 1892 "color": new DataConstantProperty(latest["light"]["color"], "color"), 1893 "intensity": new DataConstantProperty(latest["light"]["intensity"], "intensity") 1894}); 1895//#endregion 1896//#region src/style/light.ts 1897var Light = class extends Evented { 1898 constructor(lightOptions, globalState) { 1899 super(); 1900 this._transitionable = new Transitionable(getProperties$2(), "light", globalState); 1901 this.setLight(lightOptions); 1902 this._transitioning = this._transitionable.untransitioned(); 1903 } 1904 getLight() { 1905 return this._transitionable.serialize(); 1906 } 1907 /** 1908 * Gets the light position in cartesian coordinates. 1909 */ 1910 getCartesianPosition() { 1911 return sphericalToCartesian(this.properties.get("position")); 1912 } 1913 setLight(light, options = {}) { 1914 if (this._validate(validateStyle.light, light, options)) return; 1915 this._transitionable.setValues(light); 1916 } 1917 updateTransitions(parameters) { 1918 this._transitioning = this._transitionable.transitioned(parameters, this._transitioning); 1919 } 1920 hasTransition() { 1921 return this._transitioning.hasTransition(); 1922 } 1923 applyGlobalStateChange(refs, priorGlobalState, parameters) { 1924 this._transitioning = this._transitionable.applyGlobalStateChange(refs, priorGlobalState, this._transitioning, parameters); 1925 } 1926 recalculate(parameters) { 1927 this.properties = this._transitioning.possiblyEvaluate(parameters); 1928 } 1929 _validate(validate, value, options) { 1930 return validateAndEmit(this, validate, { value }, options); 1931 } 1932}; 1933//#endregion 1934//#region src/style/sky_properties.g.ts 1935let properties$1; 1936const getProperties$1 = () => properties$1 = properties$1 || new Properties({ 1937 "sky-color": new DataConstantProperty(latest["sky"]["sky-color"], "sky-color"), 1938 "horizon-color": new DataConstantProperty(latest["sky"]["horizon-color"], "horizon-color"), 1939 "fog-color": new DataConstantProperty(latest["sky"]["fog-color"], "fog-color"), 1940 "fog-ground-blend": new DataConstantProperty(latest["sky"]["fog-ground-blend"], "fog-ground-blend"), 1941 "horizon-fog-blend": new DataConstantProperty(latest["sky"]["horizon-fog-blend"], "horizon-fog-blend"), 1942 "sky-horizon-blend": new DataConstantProperty(latest["sky"]["sky-horizon-blend"], "sky-horizon-blend"), 1943 "atmosphere-blend": new DataConstantProperty(latest["sky"]["atmosphere-blend"], "atmosphere-blend") 1944}); 1945//#endregion 1946//#region src/style/sky.ts 1947var Sky = class extends Evented { 1948 constructor(sky, globalState) { 1949 super(); 1950 this._transitionable = new Transitionable(getProperties$1(), "sky", globalState); 1951 this.setSky(sky); 1952 this._transitioning = this._transitionable.untransitioned(); 1953 this.recalculate(new EvaluationParameters(0)); 1954 } 1955 /** 1956 * Validates and applies the sky. Returns false, after firing `error`, when 1957 * the value was rejected and nothing changed. 1958 */ 1959 setSky(sky, options = {}) { 1960 if (this._validate(validateStyle.sky, sky, options)) return false; 1961 sky ||= { 1962 "sky-color": "transparent", 1963 "horizon-color": "transparent", 1964 "fog-color": "transparent", 1965 "fog-ground-blend": 1, 1966 "atmosphere-blend": 0 1967 }; 1968 this._transitionable.setValues(sky); 1969 return true; 1970 } 1971 getSky() { 1972 return this._transitionable.serialize(); 1973 } 1974 updateTransitions(parameters) { 1975 this._transitioning = this._transitionable.transitioned(parameters, this._transitioning); 1976 } 1977 hasTransition() { 1978 return this._transitioning.hasTransition(); 1979 } 1980 applyGlobalStateChange(refs, priorGlobalState, parameters) { 1981 this._transitioning = this._transitionable.applyGlobalStateChange(refs, priorGlobalState, this._transitioning, parameters); 1982 } 1983 recalculate(parameters) { 1984 this.properties = this._transitioning.possiblyEvaluate(parameters); 1985 } 1986 _validate(validate, value, options = {}) { 1987 return validateAndEmit(this, validate, { value }, options); 1988 } 1989 /** 1990 * Currently fog is a very simple implementation, and should only used 1991 * to create an atmosphere near the horizon. 1992 * But because the fog is drawn from the far-clipping-plane to 1993 * map-center, and because the fog does nothing know about the horizon, 1994 * this method does a fadeout in respect of pitch. So, when the horizon 1995 * gets out of view, which is at about pitch 70, this methods calculates 1996 * the corresponding opacity values. Below pitch 60 the fog is completely 1997 * invisible. 1998 */ 1999 calculateFogBlendOpacity(pitch) { 2000 if (pitch < 60) return 0; 2001 if (pitch < 70) return (pitch - 60) / 10; 2002 return 1; 2003 } 2004}; 2005//#endregion 2006//#region src/render/line_atlas.ts 2007/** 2008* @internal 2009* A LineAtlas lets us reuse rendered dashed lines 2010* by writing many of them to a texture and then fetching their positions 2011* using {@link LineAtlas.getDash}. 2012* 2013* @param width - the width 2014* @param height - the height 2015*/ 2016var LineAtlas = class { 2017 constructor(width, height) { 2018 this.width = width; 2019 this.height = height; 2020 this.nextRow = 0; 2021 this.data = new Uint8Array(this.width * this.height); 2022 this.dashEntry = {}; 2023 } 2024 /** 2025 * Get or create a dash line pattern. 2026 * 2027 * @param dasharray - the key (represented by numbers) to get the dash texture 2028 * @param round - whether to add circle caps in between dash segments 2029 * @returns position of dash texture in {@link DashEntry} 2030 */ 2031 getDash(dasharray, round) { 2032 const key = dasharray.join(",") + String(round); 2033 this.dashEntry[key] ||= this.addDash(dasharray, round); 2034 return this.dashEntry[key]; 2035 } 2036 getDashRanges(dasharray, lineAtlasWidth, stretch) { 2037 const oddDashArray = dasharray.length % 2 === 1; 2038 const ranges = []; 2039 let left = oddDashArray ? -dasharray[dasharray.length - 1] * stretch : 0; 2040 let right = dasharray[0] * stretch; 2041 let isDash = true; 2042 ranges.push({ 2043 left, 2044 right, 2045 isDash, 2046 zeroLength: dasharray[0] === 0 2047 }); 2048 let currentDashLength = dasharray[0]; 2049 for (let i = 1; i < dasharray.length; i++) { 2050 isDash = !isDash; 2051 const dashLength = dasharray[i]; 2052 left = currentDashLength * stretch; 2053 currentDashLength += dashLength; 2054 right = currentDashLength * stretch; 2055 ranges.push({ 2056 left, 2057 right, 2058 isDash, 2059 zeroLength: dashLength === 0 2060 }); 2061 } 2062 return ranges; 2063 } 2064 addRoundDash(ranges, stretch, n) { 2065 const halfStretch = stretch / 2; 2066 for (let y = -n; y <= n; y++) { 2067 const row = this.nextRow + n + y; 2068 const index = this.width * row; 2069 let currIndex = 0; 2070 let range = ranges[currIndex]; 2071 for (let x = 0; x < this.width; x++) { 2072 if (x / range.right > 1) range = ranges[++currIndex]; 2073 const distLeft = Math.abs(x - range.left); 2074 const distRight = Math.abs(x - range.right); 2075 const minDist = Math.min(distLeft, distRight); 2076 let signedDistance; 2077 const distMiddle = y / n * (halfStretch + 1); 2078 if (range.isDash) { 2079 const distEdge = halfStretch - Math.abs(distMiddle); 2080 signedDistance = Math.sqrt(minDist * minDist + distEdge * distEdge); 2081 } else signedDistance = halfStretch - Math.sqrt(minDist * minDist + distMiddle * distMiddle); 2082 this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); 2083 } 2084 } 2085 } 2086 addRegularDash(ranges) { 2087 for (let i = ranges.length - 1; i >= 0; --i) { 2088 const part = ranges[i]; 2089 const next = ranges[i + 1]; 2090 if (part.zeroLength) ranges.splice(i, 1); 2091 else if (next?.isDash === part.isDash) { 2092 next.left = part.left; 2093 ranges.splice(i, 1); 2094 } 2095 } 2096 const first = ranges[0]; 2097 const last = ranges[ranges.length - 1]; 2098 if (first.isDash === last.isDash) { 2099 first.left = last.left - this.width; 2100 last.right = first.right + this.width; 2101 } 2102 const index = this.width * this.nextRow; 2103 let currIndex = 0; 2104 let range = ranges[currIndex]; 2105 for (let x = 0; x < this.width; x++) { 2106 if (x / range.right > 1) range = ranges[++currIndex]; 2107 const distLeft = Math.abs(x - range.left); 2108 const distRight = Math.abs(x - range.right); 2109 const minDist = Math.min(distLeft, distRight); 2110 const signedDistance = range.isDash ? minDist : -minDist; 2111 this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); 2112 } 2113 } 2114 addDash(dasharray, round) { 2115 const n = round ? 7 : 0; 2116 const height = 2 * n + 1; 2117 if (this.nextRow + height > this.height) { 2118 warnOnce("LineAtlas out of space"); 2119 return null; 2120 } 2121 let length = 0; 2122 for (const dash of dasharray) length += dash; 2123 if (length !== 0) { 2124 const stretch = this.width / length; 2125 const ranges = this.getDashRanges(dasharray, this.width, stretch); 2126 if (round) this.addRoundDash(ranges, stretch, n); 2127 else this.addRegularDash(ranges); 2128 } 2129 const dashEntry = { 2130 y: this.nextRow + n, 2131 height: 2 * n, 2132 width: length 2133 }; 2134 this.nextRow += height; 2135 this.dirty = true; 2136 return dashEntry; 2137 } 2138 bind(context) { 2139 const gl = context.gl; 2140 if (!this.texture) { 2141 this.texture = gl.createTexture(); 2142 gl.bindTexture(gl.TEXTURE_2D, this.texture); 2143 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); 2144 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); 2145 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 2146 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 2147 gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.width, this.height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); 2148 } else { 2149 gl.bindTexture(gl.TEXTURE_2D, this.texture); 2150 if (this.dirty) { 2151 this.dirty = false; 2152 gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); 2153 } 2154 } 2155 } 2156}; 2157//#endregion 2158//#region src/util/web_worker.ts 2159function isCrossOrigin(url) { 2160 if (!url) return false; 2161 const loc = globalThis.location; 2162 if (!loc) return false; 2163 try { 2164 return new URL(url, loc.href).origin !== loc.origin; 2165 } catch { 2166 return false; 2167 } 2168} 2169function defaultWorkerUrl() { 2170 const moduleUrl = import.meta.url; 2171 if (!/^https?:/.test(moduleUrl)) return ""; 2172 const workerName = moduleUrl.endsWith("-dev.mjs") ? "maplibre-gl-worker-dev.mjs" : "maplibre-gl-worker.mjs"; 2173 return new URL(`./${workerName}`, moduleUrl).href; 2174} 2175function createWorker(url, asModule) { 2176 if (asModule) try { 2177 return new Worker(url, { type: "module" }); 2178 } catch (e) { 2179 console.warn("Module worker not supported, falling back to classic worker", e); 2180 } 2181 return new Worker(url); 2182} 2183async function fetchAsBlobUrl(url) { 2184 const response = await fetch(url); 2185 if (!response.ok) throw new Error(`Failed to fetch worker script (${response.status}): ${url}`); 2186 const code = await response.text(); 2187 const blob = new Blob([code], { type: "text/javascript" }); 2188 return URL.createObjectURL(blob); 2189} 2190function importAsBlobUrl(url) { 2191 const blob = new Blob([`import ${JSON.stringify(new URL(url, import.meta.url).href)}`], { type: "text/javascript" }); 2192 return URL.createObjectURL(blob); 2193} 2194async function workerFactory() { 2195 const url = config.WORKER_URL || defaultWorkerUrl(); 2196 const asModule = url?.endsWith(".cjs") ? false : true; 2197 if (!isCrossOrigin(url)) return createWorker(url, asModule); 2198 if (asModule) { 2199 const blobUrl = importAsBlobUrl(url); 2200 try { 2201 return createWorker(blobUrl, asModule); 2202 } finally { 2203 URL.revokeObjectURL(blobUrl); 2204 } 2205 } 2206 const blobUrl = await fetchAsBlobUrl(url); 2207 try { 2208 return createWorker(blobUrl, asModule); 2209 } finally { 2210 URL.revokeObjectURL(blobUrl); 2211 } 2212} 2213//#endregion 2214//#region src/util/worker_pool.ts 2215const PRELOAD_POOL_ID = "maplibre_preloaded_worker_pool"; 2216/** 2217* Constructs a worker pool. 2218*/ 2219var WorkerPool = class WorkerPool { 2220 constructor() { 2221 this.active = {}; 2222 this.workersPromise = null; 2223 } 2224 async acquire(mapId) { 2225 this.active[mapId] = true; 2226 if (!this.workersPromise) { 2227 const promises = []; 2228 while (promises.length < WorkerPool.workerCount) promises.push(workerFactory()); 2229 this.workersPromise = Promise.all(promises); 2230 } 2231 return (await this.workersPromise).slice(); 2232 } 2233 release(mapId) { 2234 delete this.active[mapId]; 2235 if (this.numActive() === 0 && this.workersPromise) { 2236 const promise = this.workersPromise; 2237 this.workersPromise = null; 2238 promise.then((workers) => { 2239 for (const w of workers) w.terminate(); 2240 }); 2241 } 2242 } 2243 isPreloaded() { 2244 return !!this.active[PRELOAD_POOL_ID]; 2245 } 2246 numActive() { 2247 return Object.keys(this.active).length; 2248 } 2249}; 2250const availableLogicalProcessors = Math.floor(browser.hardwareConcurrency / 2); 2251WorkerPool.workerCount = isSafari(globalThis) ? Math.max(Math.min(availableLogicalProcessors, 3), 1) : 1; 2252//#endregion 2253//#region src/util/global_worker_pool.ts 2254let globalWorkerPool; 2255/** 2256* Creates (if necessary) and returns the single, global WorkerPool instance 2257* to be shared across each Map 2258*/ 2259function getGlobalWorkerPool() { 2260 globalWorkerPool ||= new WorkerPool(); 2261 return globalWorkerPool; 2262} 2263/** 2264* Initializes resources like WebWorkers that can be shared across maps to lower load 2265* times in some situations. `setWorkerUrl()` and `setWorkerCount()`, if being 2266* used, must be set before `prewarm()` is called to have an effect. 2267* 2268* By default, the lifecycle of these resources is managed automatically, and they are 2269* lazily initialized when a Map is first created. By invoking `prewarm()`, these 2270* resources will be created ahead of time, and will not be cleared when the last Map 2271* is removed from the page. This allows them to be re-used by new Map instances that 2272* are created later. They can be manually cleared by calling 2273* `clearPrewarmedResources()`. This is only necessary if your web page remains 2274* active but stops using maps altogether. 2275* 2276* This is primarily useful when using GL-JS maps in a single page app, wherein a user 2277* would navigate between various views that can cause Map instances to constantly be 2278* created and destroyed. 2279* 2280* @example 2281* ```ts 2282* prewarm() 2283* ``` 2284*/ 2285function prewarm() { 2286 getGlobalWorkerPool().acquire(PRELOAD_POOL_ID); 2287} 2288/** 2289* Clears up resources that have previously been created by `prewarm()`. 2290* Note that this is typically not necessary. You should only call this function 2291* if you expect the user of your app to not return to a Map view at any point 2292* in your application. 2293* 2294* @example 2295* ```ts 2296* clearPrewarmedResources() 2297* ``` 2298*/ 2299function clearPrewarmedResources() { 2300 const pool = globalWorkerPool; 2301 if (pool) { 2302 if (pool.isPreloaded() && pool.numActive() === 1) { 2303 pool.release(PRELOAD_POOL_ID); 2304 globalWorkerPool = null; 2305 } else console.warn("Could not clear WebWorkers since there are active Map instances that still reference it. The pre-warmed WebWorker pool can only be cleared when all map instances have been removed with map.remove()"); 2306 } 2307} 2308//#endregion 2309//#region src/util/dispatcher.ts 2310/** 2311* Responsible for sending messages from a {@link Source} to an associated worker source (usually with the same name). 2312*/ 2313var Dispatcher = class extends Evented { 2314 constructor(workerPool, mapId) { 2315 super(); 2316 this.workerPool = workerPool; 2317 this.actors = []; 2318 this.currentActor = 0; 2319 this.id = mapId; 2320 this.removed = false; 2321 this.workerErrorSubscriptions = []; 2322 this.actorsPromise = this.initActors(mapId); 2323 } 2324 async initActors(mapId) { 2325 const workers = await this.workerPool.acquire(mapId); 2326 if (this.removed) return []; 2327 this.actors = workers.map((worker, i) => { 2328 this.workerErrorSubscriptions.push(subscribe(worker, "error", () => { 2329 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Worker failed to load. Check that the worker URL is correct."))); 2330 }, false)); 2331 const actor = new Actor(worker, mapId); 2332 actor.name = `Worker ${i}`; 2333 return actor; 2334 }); 2335 if (!this.actors.length) throw new Error("No actors found"); 2336 return this.actors; 2337 } 2338 /** 2339 * Broadcast a message to all Workers. 2340 */ 2341 async broadcast(type, data) { 2342 const actors = await this.actorsPromise; 2343 return Promise.all(actors.map((actor) => actor.sendAsync({ 2344 type, 2345 data 2346 }))); 2347 } 2348 /** 2349 * Acquires an actor to dispatch messages to. The actors are distributed in round-robin fashion. 2350 * @returns An actor object backed by a web worker for processing messages. 2351 */ 2352 async getActor() { 2353 const actors = await this.actorsPromise; 2354 this.currentActor = (this.currentActor + 1) % actors.length; 2355 return actors[this.currentActor]; 2356 } 2357 async waitForInitComplete() { 2358 if (this.actors.length === 0) await this.actorsPromise; 2359 } 2360 getReadyActor() { 2361 this.currentActor = (this.currentActor + 1) % this.actors.length; 2362 return this.actors[this.currentActor]; 2363 } 2364 remove(mapRemoved = true) { 2365 this.removed = true; 2366 for (const actor of this.actors) actor.remove(); 2367 for (const subscription of this.workerErrorSubscriptions) subscription.unsubscribe(); 2368 this.actors = []; 2369 this.workerErrorSubscriptions = []; 2370 this.setEventedParent(null); 2371 if (mapRemoved) this.workerPool.release(this.id); 2372 } 2373 async registerMessageHandler(type, handler) { 2374 const actors = await this.actorsPromise; 2375 for (const actor of actors) actor.registerMessageHandler(type, handler); 2376 } 2377 async unregisterMessageHandler(type) { 2378 const actors = await this.actorsPromise; 2379 for (const actor of actors) actor.unregisterMessageHandler(type); 2380 } 2381}; 2382let globalDispatcher; 2383/** 2384* This function is used to get the global dispatcher that is shared across all maps instances. 2385* It is used by the main thread to send messages to the workers, and by the workers to send messages back to the main thread. 2386* If you import a script into the worker and need to send a message to the workers to pass some parameters for example, 2387* you can use this function to get the global dispatcher and send a message to the workers. 2388* @returns The global dispatcher instance. 2389*/ 2390function getGlobalDispatcher() { 2391 if (!globalDispatcher) { 2392 globalDispatcher = new Dispatcher(getGlobalWorkerPool(), GLOBAL_DISPATCHER_ID); 2393 globalDispatcher.registerMessageHandler("GR", (_mapId, params, abortController) => { 2394 return makeRequest(params, abortController); 2395 }); 2396 } 2397 return globalDispatcher; 2398} 2399//#endregion 2400//#region src/source/query_features.ts 2401function getPixelPosMatrix(transform, tileID) { 2402 const t = create$1(); 2403 translate(t, t, [ 2404 1, 2405 1, 2406 0 2407 ]); 2408 scale(t, t, [ 2409 transform.width * .5, 2410 transform.height * .5, 2411 1 2412 ]); 2413 if (transform.calculatePosMatrix) return multiply(t, t, transform.calculatePosMatrix(tileID.toUnwrapped())); 2414 else return t; 2415} 2416function queryIncludes3DLayer(layers, styleLayers, sourceID) { 2417 if (layers) for (const layerID of layers) { 2418 const layer = styleLayers[layerID]; 2419 if (layer?.source === sourceID && layer.type === "fill-extrusion") return true; 2420 } 2421 else for (const key in styleLayers) { 2422 const layer = styleLayers[key]; 2423 if (layer.source === sourceID && layer.type === "fill-extrusion") return true; 2424 } 2425 return false; 2426} 2427function queryRenderedFeatures(tileManager, styleLayers, serializedLayers, queryGeometry, params, transform, getElevation) { 2428 const has3DLayer = queryIncludes3DLayer(params?.layers ?? null, styleLayers, tileManager.id); 2429 const maxPitchScaleFactor = transform.maxPitchScaleFactor(); 2430 const tilesIn = tileManager.tilesIn(queryGeometry, maxPitchScaleFactor, has3DLayer); 2431 tilesIn.sort(sortTilesIn); 2432 const renderedFeatureLayers = []; 2433 for (const tileIn of tilesIn) renderedFeatureLayers.push({ 2434 wrappedTileID: tileIn.tileID.wrapped().key, 2435 queryResults: tileIn.tile.queryRenderedFeatures(styleLayers, serializedLayers, tileManager.getState(), tileIn.queryGeometry, tileIn.cameraQueryGeometry, tileIn.scale, params, transform, maxPitchScaleFactor, getPixelPosMatrix(transform, tileIn.tileID), getElevation ? (x, y) => getElevation(tileIn.tileID, x, y) : void 0) 2436 }); 2437 return convertFeaturesToMapFeatures(mergeRenderedFeatureLayers(renderedFeatureLayers), tileManager); 2438} 2439function queryRenderedSymbols(styleLayers, serializedLayers, tileManagers, queryGeometry, params, collisionIndex, retainedQueryData) { 2440 const result = {}; 2441 const renderedSymbols = collisionIndex.queryRenderedSymbols(queryGeometry); 2442 const bucketQueryData = []; 2443 for (const bucketInstanceId of Object.keys(renderedSymbols).map(Number)) bucketQueryData.push(retainedQueryData[bucketInstanceId]); 2444 bucketQueryData.sort(sortTilesIn); 2445 for (const queryData of bucketQueryData) { 2446 const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures(renderedSymbols[queryData.bucketInstanceId], serializedLayers, queryData.bucketIndex, queryData.sourceLayerIndex, { 2447 filterSpec: params.filter, 2448 globalState: params.globalState 2449 }, params.layers, params.availableImages, styleLayers); 2450 for (const layerID in bucketSymbols) { 2451 result[layerID] ||= []; 2452 const layerSymbols = bucketSymbols[layerID]; 2453 layerSymbols.sort((a, b) => { 2454 const featureSortOrder = queryData.featureSortOrder; 2455 if (featureSortOrder) { 2456 const sortedA = featureSortOrder.indexOf(a.featureIndex); 2457 return featureSortOrder.indexOf(b.featureIndex) - sortedA; 2458 } else return b.featureIndex - a.featureIndex; 2459 }); 2460 for (const symbolFeature of layerSymbols) result[layerID].push(symbolFeature); 2461 } 2462 } 2463 return convertFeaturesToMapFeaturesMultiple(result, styleLayers, tileManagers); 2464} 2465function querySourceFeatures(tileManager, params) { 2466 const tiles = tileManager.getRenderableIds().map((id) => { 2467 return tileManager.getTileByID(id); 2468 }); 2469 const result = []; 2470 const dataTiles = {}; 2471 for (const tile of tiles) { 2472 const dataID = tile.tileID.canonical.key; 2473 if (!dataTiles[dataID]) { 2474 dataTiles[dataID] = true; 2475 tile.querySourceFeatures(result, params); 2476 } 2477 } 2478 return result; 2479} 2480function sortTilesIn(a, b) { 2481 const idA = a.tileID; 2482 const idB = b.tileID; 2483 return idA.overscaledZ - idB.overscaledZ || idA.canonical.y - idB.canonical.y || idA.wrap - idB.wrap || idA.canonical.x - idB.canonical.x; 2484} 2485function mergeRenderedFeatureLayers(tiles) { 2486 const result = {}; 2487 const wrappedIDLayerMap = {}; 2488 for (const { queryResults, wrappedTileID } of tiles) { 2489 wrappedIDLayerMap[wrappedTileID] ||= {}; 2490 const wrappedIDLayers = wrappedIDLayerMap[wrappedTileID]; 2491 for (const layerID in queryResults) { 2492 const tileFeatures = queryResults[layerID]; 2493 wrappedIDLayers[layerID] ||= {}; 2494 const wrappedIDFeatures = wrappedIDLayers[layerID]; 2495 result[layerID] ||= []; 2496 for (const tileFeature of tileFeatures) if (!wrappedIDFeatures[tileFeature.featureIndex]) { 2497 wrappedIDFeatures[tileFeature.featureIndex] = true; 2498 result[layerID].push(tileFeature); 2499 } 2500 } 2501 } 2502 return result; 2503} 2504function convertFeaturesToMapFeatures(result, tileManager) { 2505 for (const layerID in result) for (const featureWrapper of result[layerID]) convertFeatureToMapFeature(featureWrapper, tileManager); 2506 return result; 2507} 2508function convertFeaturesToMapFeaturesMultiple(result, styleLayers, tileManagers) { 2509 for (const layerName in result) for (const featureWrapper of result[layerName]) { 2510 const tileManager = tileManagers[styleLayers[layerName].source]; 2511 convertFeatureToMapFeature(featureWrapper, tileManager); 2512 } 2513 return result; 2514} 2515function convertFeatureToMapFeature(featureWrapper, tileManager) { 2516 const feature = featureWrapper.feature; 2517 const state = tileManager.getFeatureState(feature.layer["source-layer"], feature.id); 2518 feature.source = feature.layer.source; 2519 if (feature.layer["source-layer"]) feature.sourceLayer = feature.layer["source-layer"]; 2520 feature.state = state; 2521} 2522//#endregion 2523//#region src/source/load_tilejson.ts 2524async function loadTileJson(options, requestManager, abortController, targetWindow) { 2525 let tileJSON = options; 2526 if (options.url) tileJSON = (await getJSON(await requestManager.transformRequest(options.url, "Source"), abortController)).data; 2527 else await browser.frameAsync(abortController, targetWindow); 2528 if (!tileJSON) return null; 2529 const result = pick(extend(tileJSON, options), [ 2530 "tiles", 2531 "minzoom", 2532 "maxzoom", 2533 "attribution", 2534 "bounds", 2535 "scheme", 2536 "tileSize", 2537 "encoding" 2538 ]); 2539 if ("vector_layers" in tileJSON && tileJSON.vector_layers) result.vectorLayerIds = tileJSON.vector_layers.map((layer) => layer.id); 2540 return result; 2541} 2542//#endregion 2543//#region src/geo/lng_lat_bounds.ts 2544/** 2545* A `LngLatBounds` object represents a geographical bounding box, 2546* defined by its southwest and northeast points in longitude and latitude. 2547* 2548* If no arguments are provided to the constructor, a `null` bounding box is created. 2549* 2550* Note that any MapLibre GL method that accepts a `LngLatBounds` object as an argument or option 2551* can also accept an `Array` of two {@link LngLatLike} constructs and will perform an implicit conversion. 2552* This flexible type is documented as {@link LngLatBoundsLike}. 2553* 2554* @group Geography and Geometry 2555* 2556* @example 2557* ```ts 2558* let sw = new LngLat(-73.9876, 40.7661); 2559* let ne = new LngLat(-73.9397, 40.8002); 2560* let llb = new LngLatBounds(sw, ne); 2561* ``` 2562* @see [Fit to the bounds of a LineString](https://maplibre.org/maplibre-gl-js/docs/examples/fit-to-the-bounds-of-a-linestring/) 2563*/ 2564var LngLatBounds = class LngLatBounds { 2565 /** 2566 * @param sw - The southwest corner of the bounding box. 2567 * OR array of 4 numbers in the order of west, south, east, north 2568 * OR array of 2 LngLatLike: `[sw, ne]` 2569 * @param ne - The northeast corner of the bounding box. 2570 * @example 2571 * ```ts 2572 * let sw = new LngLat(-73.9876, 40.7661); 2573 * let ne = new LngLat(-73.9397, 40.8002); 2574 * let llb = new LngLatBounds(sw, ne); 2575 * ``` 2576 * OR 2577 * ```ts 2578 * let llb = new LngLatBounds([-73.9876, 40.7661, -73.9397, 40.8002]); 2579 * ``` 2580 * OR 2581 * ```ts 2582 * let llb = new LngLatBounds([sw, ne]); 2583 * ``` 2584 */ 2585 constructor(sw, ne) { 2586 if (!sw) {} else if (ne) this.setSouthWest(sw).setNorthEast(ne); 2587 else if (Array.isArray(sw)) { 2588 if (sw.length === 4) this.setSouthWest([sw[0], sw[1]]).setNorthEast([sw[2], sw[3]]); 2589 else this.setSouthWest(sw[0]).setNorthEast(sw[1]); 2590 } 2591 } 2592 /** 2593 * Set the northeast corner of the bounding box 2594 * 2595 * @param ne - a {@link LngLatLike} object describing the northeast corner of the bounding box. 2596 */ 2597 setNorthEast(ne) { 2598 this._ne = ne instanceof LngLat ? new LngLat(ne.lng, ne.lat) : LngLat.convert(ne); 2599 return this; 2600 } 2601 /** 2602 * Set the southwest corner of the bounding box 2603 * 2604 * @param sw - a {@link LngLatLike} object describing the southwest corner of the bounding box. 2605 */ 2606 setSouthWest(sw) { 2607 this._sw = sw instanceof LngLat ? new LngLat(sw.lng, sw.lat) : LngLat.convert(sw); 2608 return this; 2609 } 2610 /** 2611 * Extend the bounds to include a given LngLatLike or LngLatBoundsLike. 2612 * 2613 * @param obj - object to extend to 2614 */ 2615 extend(obj) { 2616 const sw = this._sw, ne = this._ne; 2617 let sw2, ne2; 2618 if (obj instanceof LngLat) { 2619 sw2 = obj; 2620 ne2 = obj; 2621 } else if (obj instanceof LngLatBounds) { 2622 sw2 = obj._sw; 2623 ne2 = obj._ne; 2624 if (!sw2 || !ne2) return this; 2625 } else { 2626 if (Array.isArray(obj)) { 2627 if (obj.length === 4 || obj.every(Array.isArray)) { 2628 const lngLatBoundsObj = obj; 2629 return this.extend(LngLatBounds.convert(lngLatBoundsObj)); 2630 } else { 2631 const lngLatObj = obj; 2632 return this.extend(LngLat.convert(lngLatObj)); 2633 } 2634 } else if (obj && ("lng" in obj || "lon" in obj) && "lat" in obj) return this.extend(LngLat.convert(obj)); 2635 return this; 2636 } 2637 if (!sw && !ne) { 2638 this._sw = new LngLat(sw2.lng, sw2.lat); 2639 this._ne = new LngLat(ne2.lng, ne2.lat); 2640 } else { 2641 sw.lng = Math.min(sw2.lng, sw.lng); 2642 sw.lat = Math.min(sw2.lat, sw.lat); 2643 ne.lng = Math.max(ne2.lng, ne.lng); 2644 ne.lat = Math.max(ne2.lat, ne.lat); 2645 } 2646 return this; 2647 } 2648 /** 2649 * Returns the geographical coordinate equidistant from the bounding box's corners. 2650 * 2651 * @returns The bounding box's center. 2652 * @example 2653 * ```ts 2654 * let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]); 2655 * llb.getCenter(); // = LngLat {lng: -73.96365, lat: 40.78315} 2656 * ``` 2657 */ 2658 getCenter() { 2659 return new LngLat((this._sw.lng + this._ne.lng) / 2, (this._sw.lat + this._ne.lat) / 2); 2660 } 2661 /** 2662 * Returns the southwest corner of the bounding box. 2663 * 2664 * @returns The southwest corner of the bounding box. 2665 */ 2666 getSouthWest() { 2667 return this._sw; 2668 } 2669 /** 2670 * Returns the northeast corner of the bounding box. 2671 * 2672 * @returns The northeast corner of the bounding box. 2673 */ 2674 getNorthEast() { 2675 return this._ne; 2676 } 2677 /** 2678 * Returns the northwest corner of the bounding box. 2679 * 2680 * @returns The northwest corner of the bounding box. 2681 */ 2682 getNorthWest() { 2683 return new LngLat(this.getWest(), this.getNorth()); 2684 } 2685 /** 2686 * Returns the southeast corner of the bounding box. 2687 * 2688 * @returns The southeast corner of the bounding box. 2689 */ 2690 getSouthEast() { 2691 return new LngLat(this.getEast(), this.getSouth()); 2692 } 2693 /** 2694 * Returns the west edge of the bounding box. 2695 * 2696 * @returns The west edge of the bounding box. 2697 */ 2698 getWest() { 2699 return this._sw.lng; 2700 } 2701 /** 2702 * Returns the south edge of the bounding box. 2703 * 2704 * @returns The south edge of the bounding box. 2705 */ 2706 getSouth() { 2707 return this._sw.lat; 2708 } 2709 /** 2710 * Returns the east edge of the bounding box. 2711 * 2712 * @returns The east edge of the bounding box. 2713 */ 2714 getEast() { 2715 return this._ne.lng; 2716 } 2717 /** 2718 * Returns the north edge of the bounding box. 2719 * 2720 * @returns The north edge of the bounding box. 2721 */ 2722 getNorth() { 2723 return this._ne.lat; 2724 } 2725 /** 2726 * Returns the bounding box represented as an array. 2727 * 2728 * @returns The bounding box represented as an array, consisting of the 2729 * southwest and northeast coordinates of the bounding represented as arrays of numbers. 2730 * @example 2731 * ```ts 2732 * let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]); 2733 * llb.toArray(); // = [[-73.9876, 40.7661], [-73.9397, 40.8002]] 2734 * ``` 2735 */ 2736 toArray() { 2737 return [this._sw.toArray(), this._ne.toArray()]; 2738 } 2739 /** 2740 * Return the bounding box represented as a string. 2741 * 2742 * @returns The bounding box represents as a string of the format 2743 * `'LngLatBounds(LngLat(lng, lat), LngLat(lng, lat))'`. 2744 * @example 2745 * ```ts 2746 * let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]); 2747 * llb.toString(); // = "LngLatBounds(LngLat(-73.9876, 40.7661), LngLat(-73.9397, 40.8002))" 2748 * ``` 2749 */ 2750 toString() { 2751 return `LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`; 2752 } 2753 /** 2754 * Check if the bounding box is an empty/`null`-type box. 2755 * 2756 * @returns True if bounds have been defined, otherwise false. 2757 */ 2758 isEmpty() { 2759 return !(this._sw && this._ne); 2760 } 2761 /** 2762 * Check if the point is within the bounding box. 2763 * 2764 * @param lnglat - geographic point to check against. 2765 * @returns `true` if the point is within the bounding box. 2766 * @example 2767 * ```ts 2768 * let llb = new LngLatBounds( 2769 * new LngLat(-73.9876, 40.7661), 2770 * new LngLat(-73.9397, 40.8002) 2771 * ); 2772 * 2773 * let ll = new LngLat(-73.9567, 40.7789); 2774 * 2775 * console.log(llb.contains(ll)); // = true 2776 * ``` 2777 */ 2778 contains(lnglat) { 2779 const { lng, lat } = LngLat.convert(lnglat); 2780 const containsLatitude = this._sw.lat <= lat && lat <= this._ne.lat; 2781 let containsLongitude = this._sw.lng <= lng && lng <= this._ne.lng; 2782 if (this._sw.lng > this._ne.lng) containsLongitude = this._sw.lng >= lng && lng >= this._ne.lng; 2783 return containsLatitude && containsLongitude; 2784 } 2785 /** 2786 * Checks if this bounding box intersects with another bounding box. 2787 * 2788 * Returns true if the bounding boxes share any area, including cases where 2789 * they only touch along an edge or at a corner. 2790 * 2791 * This method properly handles cases where either or both bounding boxes cross 2792 * the antimeridian (date line). 2793 */ 2794 intersects(other) { 2795 other = LngLatBounds.convert(other); 2796 if (!(other.getNorth() >= this.getSouth() && other.getSouth() <= this.getNorth())) return false; 2797 const thisSpan = Math.abs(this.getEast() - this.getWest()); 2798 const otherSpan = Math.abs(other.getEast() - other.getWest()); 2799 if (thisSpan >= 360 || otherSpan >= 360) return true; 2800 const thisWest = wrap(this.getWest(), -180, 180); 2801 const thisEast = wrap(this.getEast(), -180, 180); 2802 const otherWest = wrap(other.getWest(), -180, 180); 2803 const otherEast = wrap(other.getEast(), -180, 180); 2804 const thisWraps = thisWest > thisEast; 2805 const otherWraps = otherWest > otherEast; 2806 if (thisWraps && otherWraps) return true; 2807 if (thisWraps) return otherEast >= thisWest || otherWest <= thisEast; 2808 if (otherWraps) return thisEast >= otherWest || thisWest <= otherEast; 2809 return otherWest <= thisEast && otherEast >= thisWest; 2810 } 2811 /** 2812 * Converts an array to a `LngLatBounds` object. 2813 * 2814 * If a `LngLatBounds` object is passed in, the function returns it unchanged. 2815 * 2816 * Internally, the function calls {@link LngLat.convert} to convert arrays to `LngLat` values. 2817 * 2818 * @param input - An array of two coordinates to convert, or a `LngLatBounds` object to return. 2819 * @returns A new `LngLatBounds` object, if a conversion occurred, or the original `LngLatBounds` object. 2820 * @example 2821 * ```ts 2822 * let arr = [[-73.9876, 40.7661], [-73.9397, 40.8002]]; 2823 * let llb = LngLatBounds.convert(arr); // = LngLatBounds {_sw: LngLat {lng: -73.9876, lat: 40.7661}, _ne: LngLat {lng: -73.9397, lat: 40.8002}} 2824 * ``` 2825 */ 2826 static convert(input) { 2827 if (input instanceof LngLatBounds) return input; 2828 if (!input) return input; 2829 return new LngLatBounds(input); 2830 } 2831 /** 2832 * Returns a `LngLatBounds` from the coordinates extended by a given `radius`. The returned `LngLatBounds` completely contains the `radius`. 2833 * 2834 * @param center - center coordinates of the new bounds. 2835 * @param radius - Distance in meters from the coordinates to extend the bounds. 2836 * @returns A new `LngLatBounds` object representing the coordinates extended by the `radius`. 2837 * @example 2838 * ```ts 2839 * let center = new LngLat(-73.9749, 40.7736); 2840 * LngLatBounds.fromLngLat(100).toArray(); // = [[-73.97501862141328, 40.77351016847229], [-73.97478137858673, 40.77368983152771]] 2841 * ``` 2842 */ 2843 static fromLngLat(center, radius = 0) { 2844 const latAccuracy = 360 * radius / 40075017, lngAccuracy = latAccuracy / Math.cos(Math.PI / 180 * center.lat); 2845 return new LngLatBounds(new LngLat(center.lng - lngAccuracy, center.lat - latAccuracy), new LngLat(center.lng + lngAccuracy, center.lat + latAccuracy)); 2846 } 2847 /** 2848 * Adjusts the given bounds to handle the case where the bounds cross the 180th meridian (antimeridian). 2849 * 2850 * @returns The adjusted LngLatBounds 2851 * @example 2852 * ```ts 2853 * let bounds = new LngLatBounds([175.813127, -20.157768], [-178. 340903, -15.449124]); 2854 * let adjustedBounds = bounds.adjustAntiMeridian(); 2855 * // adjustedBounds will be: [[175.813127, -20.157768], [181.659097, -15.449124]] 2856 * ``` 2857 */ 2858 adjustAntiMeridian() { 2859 const sw = new LngLat(this._sw.lng, this._sw.lat); 2860 const ne = new LngLat(this._ne.lng, this._ne.lat); 2861 if (sw.lng > ne.lng) return new LngLatBounds(sw, new LngLat(ne.lng + 360, ne.lat)); 2862 return new LngLatBounds(sw, ne); 2863 } 2864}; 2865//#endregion 2866//#region src/tile/tile_bounds.ts 2867var TileBounds = class { 2868 constructor(bounds, minzoom, maxzoom) { 2869 this.bounds = LngLatBounds.convert(this.validateBounds(bounds)); 2870 this.minzoom = minzoom || 0; 2871 this.maxzoom = maxzoom || 24; 2872 } 2873 validateBounds(bounds) { 2874 if (!Array.isArray(bounds) || bounds.length !== 4) return [ 2875 -180, 2876 -90, 2877 180, 2878 90 2879 ]; 2880 return [ 2881 Math.max(-180, bounds[0]), 2882 Math.max(-90, bounds[1]), 2883 Math.min(180, bounds[2]), 2884 Math.min(90, bounds[3]) 2885 ]; 2886 } 2887 contains(tileID) { 2888 const worldSize = Math.pow(2, tileID.z); 2889 const level = { 2890 minX: Math.floor(mercatorXfromLng(this.bounds.getWest()) * worldSize), 2891 minY: Math.floor(mercatorYfromLat(this.bounds.getNorth()) * worldSize), 2892 maxX: Math.ceil(mercatorXfromLng(this.bounds.getEast()) * worldSize), 2893 maxY: Math.ceil(mercatorYfromLat(this.bounds.getSouth()) * worldSize) 2894 }; 2895 return tileID.x >= level.minX && tileID.x < level.maxX && tileID.y >= level.minY && tileID.y < level.maxY; 2896 } 2897}; 2898//#endregion 2899//#region src/source/vector_tile_source.ts 2900/** 2901* A source containing vector tiles in [Maplibre Vector Tile format](https://maplibre.org/maplibre-tile-spec/) or [Mapbox Vector Tile format](https://docs.mapbox.com/vector-tiles/reference/). 2902* (See the [Style Specification](https://maplibre.org/maplibre-style-spec/) for detailed documentation of options.) 2903* 2904* @group Sources 2905* 2906* @example 2907* ```ts 2908* map.addSource('some id', { 2909* type: 'vector', 2910* url: 'https://demotiles.maplibre.org/tiles/tiles.json' 2911* }); 2912* ``` 2913* 2914* @example 2915* ```ts 2916* map.addSource('some id', { 2917* type: 'vector', 2918* tiles: ['https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt'], 2919* minzoom: 6, 2920* maxzoom: 14 2921* }); 2922* ``` 2923* 2924* @example 2925* ```ts 2926* map.getSource('some id').setUrl("https://demotiles.maplibre.org/tiles/tiles.json"); 2927* ``` 2928* 2929* @example 2930* ```ts 2931* map.getSource('some id').setTiles(['https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt']); 2932* ``` 2933* @see [Add a vector tile source](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-vector-tile-source/) 2934*/ 2935var VectorTileSource = class extends Evented { 2936 constructor(id, options, dispatcher, eventedParent) { 2937 super(); 2938 this.id = id; 2939 this.dispatcher = dispatcher; 2940 this.type = "vector"; 2941 this.minzoom = 0; 2942 this.maxzoom = 22; 2943 this.scheme = "xyz"; 2944 this.tileSize = 512; 2945 this.reparseOverscaled = true; 2946 this.isTileClipped = true; 2947 this._loaded = false; 2948 extend(this, pick(options, [ 2949 "url", 2950 "scheme", 2951 "tileSize", 2952 "promoteId", 2953 "encoding" 2954 ])); 2955 this._options = extend({ type: "vector" }, options); 2956 this._collectResourceTiming = options.collectResourceTiming; 2957 if (this.tileSize !== 512) throw new Error("vector tile sources must have a tileSize of 512"); 2958 this.setEventedParent(eventedParent); 2959 } 2960 async load(sourceDataChanged = false) { 2961 this._loaded = false; 2962 this.fire(new MapSourceDataEvent("dataloading")); 2963 this._tileJSONRequest = new AbortController(); 2964 try { 2965 const tileJSON = await loadTileJson(this._options, this.map._requestManager, this._tileJSONRequest, this.map._ownerWindow); 2966 this._tileJSONRequest = null; 2967 this._loaded = true; 2968 if (tileJSON) { 2969 extend(this, tileJSON); 2970 if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); 2971 this.fire(new MapSourceDataEvent("data", { sourceDataType: "metadata" })); 2972 this.fire(new MapSourceDataEvent("data", { 2973 sourceDataType: "content", 2974 sourceDataChanged 2975 })); 2976 } 2977 } catch (err) { 2978 this._tileJSONRequest = null; 2979 this._loaded = true; 2980 if (!isAbortError(err)) this.fire(new ErrorEvent(ensureError(err))); 2981 } 2982 } 2983 loaded() { 2984 return this._loaded; 2985 } 2986 hasTile(tileID) { 2987 return !this.tileBounds || this.tileBounds.contains(tileID.canonical); 2988 } 2989 onAdd(map) { 2990 this.map = map; 2991 this.load(); 2992 } 2993 setSourceProperty(callback) { 2994 if (this._tileJSONRequest) this._tileJSONRequest.abort(); 2995 callback(); 2996 this.load(true); 2997 } 2998 /** 2999 * Sets the source `tiles` property and re-renders the map. 3000 * 3001 * @param tiles - An array of one or more tile source URLs, as in the TileJSON spec. 3002 */ 3003 setTiles(tiles) { 3004 this.setSourceProperty(() => { 3005 this.tiles = tiles; 3006 this._options.tiles = tiles; 3007 }); 3008 return this; 3009 } 3010 /** 3011 * Sets the source `url` property and re-renders the map. 3012 * 3013 * @param url - A URL to a TileJSON resource. Supported protocols are `http:` and `https:`. 3014 */ 3015 setUrl(url) { 3016 this.setSourceProperty(() => { 3017 this.url = url; 3018 this._options.url = url; 3019 }); 3020 return this; 3021 } 3022 onRemove() { 3023 if (this._tileJSONRequest) { 3024 this._tileJSONRequest.abort(); 3025 this._tileJSONRequest = null; 3026 } 3027 } 3028 serialize() { 3029 return extend({}, this._options); 3030 } 3031 async loadTile(tile) { 3032 const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); 3033 const params = { 3034 request: await this.map._requestManager.transformRequest(url, "Tile"), 3035 uid: tile.uid, 3036 tileID: tile.tileID, 3037 zoom: tile.tileID.overscaledZ, 3038 tileSize: this.tileSize * tile.tileID.overscaleFactor(), 3039 type: this.type, 3040 source: this.id, 3041 pixelRatio: this.map.getPixelRatio(), 3042 showCollisionBoxes: this.map.showCollisionBoxes, 3043 promoteId: this.promoteId, 3044 subdivisionGranularity: this.map.style.projection.subdivisionGranularity, 3045 encoding: this.encoding, 3046 overzoomParameters: await this._getOverzoomParameters(tile), 3047 etag: tile.etag 3048 }; 3049 params.request.collectResourceTiming = this._collectResourceTiming; 3050 await this.dispatcher.waitForInitComplete(); 3051 if (tile.aborted) return; 3052 let messageType = "RT"; 3053 if (!tile.actor || tile.state === "expired") { 3054 tile.actor = this.dispatcher.getReadyActor(); 3055 messageType = "LT"; 3056 } else if (tile.state === "loading") return new Promise((resolve, reject) => { 3057 tile.reloadPromise = { 3058 resolve, 3059 reject 3060 }; 3061 }); 3062 tile.abortController = new AbortController(); 3063 try { 3064 const data = await tile.actor.sendAsync({ 3065 type: messageType, 3066 data: params 3067 }, tile.abortController); 3068 delete tile.abortController; 3069 if (tile.aborted) return; 3070 this._afterTileLoadWorkerResponse(tile, data); 3071 const result = {}; 3072 if (data?.etagUnmodified) result.unmodified = true; 3073 return result; 3074 } catch (err) { 3075 delete tile.abortController; 3076 if (tile.aborted || isAbortError(err)) return; 3077 if (err && err.status !== 404) throw err; 3078 this._afterTileLoadWorkerResponse(tile, null); 3079 } 3080 } 3081 /** 3082 * When the requested tile has a higher canonical Z than source maxzoom, pass overzoom parameters so worker can load the 3083 * deepest tile at source max zoom to generate sub tiles using geojsonvt for highest performance on vector overscaling 3084 */ 3085 async _getOverzoomParameters(tile) { 3086 if (tile.tileID.canonical.z <= this.maxzoom) return; 3087 if (this.map._zoomLevelsToOverscale === void 0) return; 3088 const maxZoomTileID = tile.tileID.scaledTo(this.maxzoom).canonical; 3089 const maxZoomTileUrl = maxZoomTileID.url(this.tiles, this.map.getPixelRatio(), this.scheme); 3090 return { 3091 maxZoomTileID, 3092 overzoomRequest: await this.map._requestManager.transformRequest(maxZoomTileUrl, "Tile") 3093 }; 3094 } 3095 _afterTileLoadWorkerResponse(tile, data) { 3096 if (data?.resourceTiming) tile.resourceTiming = data.resourceTiming; 3097 if (data && this.map._refreshExpiredTiles) tile.setExpiryData(data); 3098 tile.etag = data?.etag; 3099 tile.loadVectorData(data, this.map.painter); 3100 if (tile.reloadPromise) { 3101 const reloadPromise = tile.reloadPromise; 3102 tile.reloadPromise = null; 3103 this.loadTile(tile).then(reloadPromise.resolve).catch(reloadPromise.reject); 3104 } 3105 } 3106 async abortTile(tile) { 3107 if (tile.abortController) { 3108 tile.abortController.abort(); 3109 delete tile.abortController; 3110 } 3111 if (tile.actor) await tile.actor.sendAsync({ 3112 type: "AT", 3113 data: { 3114 uid: tile.uid, 3115 type: this.type, 3116 source: this.id 3117 } 3118 }); 3119 } 3120 async unloadTile(tile) { 3121 tile.unloadVectorData(); 3122 if (tile.actor) await tile.actor.sendAsync({ 3123 type: "RMT", 3124 data: { 3125 uid: tile.uid, 3126 type: this.type, 3127 source: this.id 3128 } 3129 }); 3130 } 3131 hasTransition() { 3132 return false; 3133 } 3134}; 3135//#endregion 3136//#region src/source/raster_tile_source.ts 3137/** 3138* A source containing raster tiles (See the [raster source documentation](https://maplibre.org/maplibre-style-spec/sources/#raster) for detailed documentation of options.) 3139* 3140* @group Sources 3141* 3142* \> â¹ï¸ **Note:** The default `tileSize` is `512`. If your tile provider (such as OpenStreetMap or Stadia Maps) serves 256px tiles, set `tileSize: 256` manually to avoid blurry rendering due to upscaling. 3143* 3144* @example 3145* ```ts 3146* map.addSource('raster-source', { 3147* 'type': 'raster', 3148* 'tiles': ['https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg'], 3149* 'tileSize': 256, // Set this to match tile server output to avoid blurry rendering 3150* }); 3151* ``` 3152* 3153* @example 3154* ```ts 3155* map.addSource('wms-test-source', { 3156* 'type': 'raster', 3157* // use the tiles option to specify a WMS tile source URL 3158* 'tiles': [ 3159* 'https://img.nj.gov/imagerywms/Natural2015?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=Natural2015' 3160* ], 3161* 'tileSize': 256 // Important for WMS if tiles are 256px 3162* }); 3163* ``` 3164* @see [Add a raster tile source](https://maplibre.org/maplibre-gl-js/docs/examples/map-tiles/) 3165* @see [Add a WMS source](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-wms-source/) 3166* @see [Display a satellite map](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-satellite-map/) 3167*/ 3168var RasterTileSource = class extends Evented { 3169 constructor(id, options, dispatcher, eventedParent) { 3170 super(); 3171 this.id = id; 3172 this.dispatcher = dispatcher; 3173 this.setEventedParent(eventedParent); 3174 this.type = "raster"; 3175 this.minzoom = 0; 3176 this.maxzoom = 22; 3177 this.roundZoom = true; 3178 this.scheme = "xyz"; 3179 this.tileSize = 512; 3180 this._loaded = false; 3181 this._premultiplyAlpha = true; 3182 this._options = extend({ type: "raster" }, options); 3183 extend(this, pick(options, [ 3184 "url", 3185 "scheme", 3186 "tileSize" 3187 ])); 3188 } 3189 async load(sourceDataChanged = false) { 3190 this._loaded = false; 3191 this.fire(new MapSourceDataEvent("dataloading")); 3192 this._tileJSONRequest = new AbortController(); 3193 try { 3194 const tileJSON = await loadTileJson(this._options, this.map._requestManager, this._tileJSONRequest, this.map._ownerWindow); 3195 this._tileJSONRequest = null; 3196 this._loaded = true; 3197 if (tileJSON) { 3198 extend(this, tileJSON); 3199 if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); 3200 this.fire(new MapSourceDataEvent("data", { sourceDataType: "metadata" })); 3201 this.fire(new MapSourceDataEvent("data", { 3202 sourceDataType: "content", 3203 sourceDataChanged 3204 })); 3205 } 3206 } catch (err) { 3207 this._tileJSONRequest = null; 3208 this._loaded = true; 3209 if (!isAbortError(err)) this.fire(new ErrorEvent(ensureError(err))); 3210 } 3211 } 3212 loaded() { 3213 return this._loaded; 3214 } 3215 onAdd(map) { 3216 this.map = map; 3217 this.load(); 3218 } 3219 onRemove() { 3220 if (this._tileJSONRequest) { 3221 this._tileJSONRequest.abort(); 3222 this._tileJSONRequest = null; 3223 } 3224 } 3225 setSourceProperty(callback) { 3226 if (this._tileJSONRequest) { 3227 this._tileJSONRequest.abort(); 3228 this._tileJSONRequest = null; 3229 } 3230 callback(); 3231 this.load(true); 3232 } 3233 /** 3234 * Sets the source `tiles` property and re-renders the map. 3235 * 3236 * @param tiles - An array of one or more tile source URLs, as in the raster tiles spec (See the [Style Specification](https://maplibre.org/maplibre-style-spec/) 3237 */ 3238 setTiles(tiles) { 3239 this.setSourceProperty(() => { 3240 this.tiles = tiles; 3241 this._options.tiles = tiles; 3242 }); 3243 return this; 3244 } 3245 /** 3246 * Sets the source `url` property and re-renders the map. 3247 * 3248 * @param url - A URL to a TileJSON resource. Supported protocols are `http:` and `https:`. 3249 */ 3250 setUrl(url) { 3251 this.setSourceProperty(() => { 3252 this.url = url; 3253 this._options.url = url; 3254 }); 3255 return this; 3256 } 3257 serialize() { 3258 return extend({}, this._options); 3259 } 3260 /** 3261 * Sets whether alpha premultiplication is applied to raster tile images. 3262 * Set to `false` to preserve exact RGBA byte values when alpha carries data instead of opacity. 3263 * 3264 * @param premultiplyAlpha - If `false`, disables alpha premultiplication for raster tile image decode and texture upload. 3265 * @example 3266 * ```ts 3267 * map.getSource<RasterTileSource>('raster-source').setPremultiplyAlpha(false); 3268 * ``` 3269 */ 3270 setPremultiplyAlpha(premultiplyAlpha) { 3271 if (this._premultiplyAlpha === premultiplyAlpha) return this; 3272 this.setSourceProperty(() => { 3273 this._premultiplyAlpha = premultiplyAlpha; 3274 }); 3275 return this; 3276 } 3277 hasTile(tileID) { 3278 return !this.tileBounds || this.tileBounds.contains(tileID.canonical); 3279 } 3280 async loadTile(tile) { 3281 const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); 3282 const premultiply = this._premultiplyAlpha; 3283 const imageBitmapOptions = premultiply ? void 0 : { premultiplyAlpha: "none" }; 3284 tile.abortController = new AbortController(); 3285 try { 3286 const response = await ImageRequest.transformAndGetImage(this.map._requestManager, url, "Tile", tile.abortController, this.map._refreshExpiredTiles, imageBitmapOptions); 3287 delete tile.abortController; 3288 if (tile.aborted) { 3289 tile.state = "unloaded"; 3290 return; 3291 } 3292 if (response) { 3293 if (this.map._refreshExpiredTiles && (response.cacheControl || response.expires)) tile.setExpiryData({ 3294 cacheControl: response.cacheControl, 3295 expires: response.expires 3296 }); 3297 const context = this.map.painter.context; 3298 const gl = context.gl; 3299 const img = response.data ?? new RGBAImage({ 3300 width: 1, 3301 height: 1 3302 }, /* @__PURE__ */ new Uint8Array(4)); 3303 tile.texture = this.map.painter.getTileTexture(img.width); 3304 if (tile.texture) tile.texture.update(img, { 3305 useMipmap: true, 3306 premultiply 3307 }); 3308 else { 3309 tile.texture = new Texture(context, img, gl.RGBA, { 3310 useMipmap: true, 3311 premultiply 3312 }); 3313 tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); 3314 } 3315 tile.state = "loaded"; 3316 } 3317 } catch (err) { 3318 delete tile.abortController; 3319 if (tile.aborted) tile.state = "unloaded"; 3320 else if (err) { 3321 tile.state = "errored"; 3322 throw err; 3323 } 3324 } 3325 } 3326 async abortTile(tile) { 3327 if (tile.abortController) { 3328 tile.abortController.abort(); 3329 delete tile.abortController; 3330 } 3331 } 3332 async unloadTile(tile) { 3333 if (tile.texture) this.map.painter.saveTileTexture(tile.texture); 3334 } 3335 hasTransition() { 3336 return false; 3337 } 3338}; 3339//#endregion 3340//#region src/source/raster_dem_tile_source.ts 3341/** 3342* A source containing raster DEM tiles (See the [Style Specification](https://maplibre.org/maplibre-style-spec/) for detailed documentation of options.) 3343* This source can be used to show hillshading and 3D terrain 3344* 3345* @group Sources 3346* 3347* @example 3348* ```ts 3349* map.addSource('raster-dem-source', { 3350* type: 'raster-dem', 3351* url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json', 3352* tileSize: 256 3353* }); 3354* ``` 3355* @see [3D Terrain](https://maplibre.org/maplibre-gl-js/docs/examples/3d-terrain/) 3356*/ 3357var RasterDEMTileSource = class extends RasterTileSource { 3358 constructor(id, options, dispatcher, eventedParent) { 3359 super(id, options, dispatcher, eventedParent); 3360 this.type = "raster-dem"; 3361 this.maxzoom = 22; 3362 this._options = extend({ type: "raster-dem" }, options); 3363 this.encoding = options.encoding || "mapbox"; 3364 this.redFactor = options.redFactor; 3365 this.greenFactor = options.greenFactor; 3366 this.blueFactor = options.blueFactor; 3367 this.baseShift = options.baseShift; 3368 } 3369 async loadTile(tile) { 3370 const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); 3371 tile.neighboringTiles = this._getNeighboringTiles(tile.tileID); 3372 tile.abortController = new AbortController(); 3373 try { 3374 const response = await ImageRequest.transformAndGetImage(this.map._requestManager, url, "Tile", tile.abortController, this.map._refreshExpiredTiles, { colorSpaceConversion: "none" }); 3375 delete tile.abortController; 3376 if (tile.aborted) { 3377 tile.state = "unloaded"; 3378 return; 3379 } 3380 if (response) { 3381 if (this.map._refreshExpiredTiles && (response.cacheControl || response.expires)) tile.setExpiryData({ 3382 cacheControl: response.cacheControl, 3383 expires: response.expires 3384 }); 3385 if (!response.data) { 3386 tile.state = "loaded"; 3387 return; 3388 } 3389 const img = response.data; 3390 const rawImageData = isImageBitmap(img) && offscreenCanvasSupported() ? img : await this.readImageNow(img); 3391 const params = { 3392 type: this.type, 3393 uid: tile.uid, 3394 source: this.id, 3395 rawImageData, 3396 encoding: this.encoding, 3397 redFactor: this.redFactor, 3398 greenFactor: this.greenFactor, 3399 blueFactor: this.blueFactor, 3400 baseShift: this.baseShift 3401 }; 3402 if (tile.actor && tile.state !== "expired" && tile.state !== "reloading") return; 3403 await this.dispatcher.waitForInitComplete(); 3404 if (!tile.actor || tile.state === "expired") tile.actor = this.dispatcher.getReadyActor(); 3405 tile.dem = await tile.actor.sendAsync({ 3406 type: "LDT", 3407 data: params 3408 }); 3409 tile.needsHillshadePrepare = true; 3410 tile.needsTerrainPrepare = true; 3411 tile.needsColorReliefPrepare = true; 3412 tile.state = "loaded"; 3413 } 3414 } catch (err) { 3415 delete tile.abortController; 3416 if (tile.aborted) tile.state = "unloaded"; 3417 else if (err) { 3418 tile.state = "errored"; 3419 throw err; 3420 } 3421 } 3422 } 3423 async readImageNow(img) { 3424 if (typeof VideoFrame !== "undefined" && isOffscreenCanvasDistorted()) { 3425 const width = img.width + 4; 3426 const height = img.height + 4; 3427 try { 3428 return new RGBAImage({ 3429 width, 3430 height 3431 }, await readImageUsingVideoFrame(img, -2, -2, width, height)); 3432 } catch {} 3433 } 3434 return browser.getImageData(img, 2); 3435 } 3436 _getNeighboringTiles(tileID) { 3437 const canonical = tileID.canonical; 3438 const dim = Math.pow(2, canonical.z); 3439 const px = (canonical.x - 1 + dim) % dim; 3440 const pxw = canonical.x === 0 ? tileID.wrap - 1 : tileID.wrap; 3441 const nx = (canonical.x + 1 + dim) % dim; 3442 const nxw = canonical.x + 1 === dim ? tileID.wrap + 1 : tileID.wrap; 3443 const neighboringTiles = {}; 3444 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, pxw, canonical.z, px, canonical.y).key] = { backfilled: false }; 3445 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, nxw, canonical.z, nx, canonical.y).key] = { backfilled: false }; 3446 if (canonical.y > 0) { 3447 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, pxw, canonical.z, px, canonical.y - 1).key] = { backfilled: false }; 3448 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, tileID.wrap, canonical.z, canonical.x, canonical.y - 1).key] = { backfilled: false }; 3449 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, nxw, canonical.z, nx, canonical.y - 1).key] = { backfilled: false }; 3450 } 3451 if (canonical.y + 1 < dim) { 3452 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, pxw, canonical.z, px, canonical.y + 1).key] = { backfilled: false }; 3453 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, tileID.wrap, canonical.z, canonical.x, canonical.y + 1).key] = { backfilled: false }; 3454 neighboringTiles[new OverscaledTileID(tileID.overscaledZ, nxw, canonical.z, nx, canonical.y + 1).key] = { backfilled: false }; 3455 } 3456 return neighboringTiles; 3457 } 3458 async unloadTile(tile) { 3459 if (tile.demTexture) this.map.painter.saveTileTexture(tile.demTexture); 3460 if (tile.fbo) { 3461 tile.fbo.destroy(); 3462 delete tile.fbo; 3463 } 3464 if (tile.dem) delete tile.dem; 3465 delete tile.neighboringTiles; 3466 tile.state = "unloaded"; 3467 if (tile.actor) await tile.actor.sendAsync({ 3468 type: "RDT", 3469 data: { 3470 type: this.type, 3471 uid: tile.uid, 3472 source: this.id 3473 } 3474 }); 3475 } 3476}; 3477//#endregion 3478//#region src/source/geojson_source_diff.ts 3479function getFeatureId(feature, promoteId) { 3480 return promoteId ? feature.properties[promoteId] : feature.id; 3481} 3482/** 3483* Converts a GeoJSON object into a map of feature IDs to GeoJSON features. 3484* @param data - The GeoJSON object to convert. 3485* @param promoteId - If set, the feature id will be set to the promoteId property value. 3486* @returns A map of feature IDs to GeoJSON features, or `undefined` if the GeoJSON object is not a valid updateable object. 3487* 3488* Features must have unique identifiers to be updateable. IDs can come from: 3489* - The feature's `id` property (standard GeoJSON) 3490* - A promoted property specified by `promoteId` (e.g., a "name" property) 3491*/ 3492function toUpdateable(data, promoteId) { 3493 const updateable = /* @__PURE__ */ new Map(); 3494 if (data == null) return updateable; 3495 if (data.type == null) return updateable; 3496 if (data.type === "Feature") { 3497 const id = getFeatureId(data, promoteId); 3498 if (id == null) return void 0; 3499 updateable.set(id, data); 3500 return updateable; 3501 } 3502 if (data.type === "FeatureCollection") { 3503 const seenIds = /* @__PURE__ */ new Set(); 3504 for (const feature of data.features) { 3505 const id = getFeatureId(feature, promoteId); 3506 if (id == null) return void 0; 3507 if (seenIds.has(id)) return void 0; 3508 seenIds.add(id); 3509 updateable.set(id, feature); 3510 } 3511 return updateable; 3512 } 3513} 3514/** 3515* Mutates updateable and applies a {@link GeoJSONSourceDiff}. Operations are processed in a specific order to ensure predictable behavior: 3516* 1. Remove operations (removeAll, remove) 3517* 2. Add operations (add) 3518* 3. Update operations (update) 3519* @returns an array of geometries that were affected by the diff - with the exception of removeAll which does not track any affected geometries. 3520*/ 3521function applySourceDiff(updateable, diff, promoteId) { 3522 const affectedGeometries = []; 3523 if (diff.removeAll) updateable.clear(); 3524 else if (diff.remove) for (const id of diff.remove) { 3525 const existing = updateable.get(id); 3526 if (!existing) continue; 3527 affectedGeometries.push(existing.geometry); 3528 updateable.delete(id); 3529 } 3530 if (diff.add) for (const feature of diff.add) { 3531 const id = getFeatureId(feature, promoteId); 3532 if (id == null) continue; 3533 const existing = updateable.get(id); 3534 if (existing) affectedGeometries.push(existing.geometry); 3535 affectedGeometries.push(feature.geometry); 3536 updateable.set(id, feature); 3537 } 3538 if (diff.update) for (const update of diff.update) { 3539 const existing = updateable.get(update.id); 3540 if (!existing) continue; 3541 const changeGeometry = !!update.newGeometry; 3542 const changeProps = update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0; 3543 if (!changeGeometry && !changeProps) continue; 3544 affectedGeometries.push(existing.geometry); 3545 const feature = { ...existing }; 3546 updateable.set(update.id, feature); 3547 if (changeGeometry) { 3548 affectedGeometries.push(update.newGeometry); 3549 feature.geometry = update.newGeometry; 3550 } 3551 if (changeProps) { 3552 if (update.removeAllProperties) feature.properties = {}; 3553 else feature.properties = { ...feature.properties || {} }; 3554 if (update.removeProperties) for (const key of update.removeProperties) delete feature.properties[key]; 3555 if (update.addOrUpdateProperties) for (const { key, value } of update.addOrUpdateProperties) feature.properties[key] = value; 3556 } 3557 } 3558 return affectedGeometries; 3559} 3560/** 3561* Merge two GeoJSONSourceDiffs, considering the order of operations as specified above (remove, add, update). 3562* 3563* `add` features of a promoteId source carry their id in a property, so `promoteId` is needed to key them. 3564*/ 3565function mergeSourceDiffs(prevDiff, nextDiff, promoteId) { 3566 if (!prevDiff) return nextDiff || {}; 3567 if (!nextDiff) return prevDiff || {}; 3568 const prev = diffToHashed(prevDiff, promoteId); 3569 const next = diffToHashed(nextDiff, promoteId); 3570 resolveMergeConflicts(prev, next); 3571 const merged = {}; 3572 if (prev.removeAll || next.removeAll) merged.removeAll = true; 3573 merged.remove = /* @__PURE__ */ new Set([...prev.remove, ...next.remove]); 3574 merged.add = new Map([...prev.add, ...next.add]); 3575 merged.update = new Map([...prev.update, ...next.update]); 3576 if (merged.remove.size && merged.add.size) for (const id of merged.add.keys()) merged.remove.delete(id); 3577 return hashedToDiff(merged); 3578} 3579/** 3580* Resolve merge conflicts between two GeoJSONSourceDiffs considering the ordering above (remove/add/update). 3581* 3582* - If you `removeAll` and then `add` features in the same diff, the added features will be kept. 3583* - Updates only apply to features that exist after removes and adds have been processed. 3584*/ 3585function resolveMergeConflicts(prev, next) { 3586 if (next.removeAll) { 3587 prev.add.clear(); 3588 prev.update.clear(); 3589 prev.remove.clear(); 3590 next.remove.clear(); 3591 } 3592 for (const id of next.remove) { 3593 prev.add.delete(id); 3594 prev.update.delete(id); 3595 } 3596 for (const [id, nextUpdate] of next.update) { 3597 const prevUpdate = prev.update.get(id); 3598 if (!prevUpdate) continue; 3599 next.update.set(id, mergeFeatureDiffs(prevUpdate, nextUpdate)); 3600 prev.update.delete(id); 3601 } 3602} 3603/** 3604* Merge two feature diffs for the same feature id, considering the order of operations as specified above (remove, add/update). 3605*/ 3606function mergeFeatureDiffs(prev, next) { 3607 const merged = { id: prev.id }; 3608 if (next.removeAllProperties) { 3609 delete prev.removeProperties; 3610 delete prev.addOrUpdateProperties; 3611 delete next.removeProperties; 3612 } 3613 if (next.removeProperties && prev.addOrUpdateProperties) { 3614 const removedProperties = new Set(next.removeProperties); 3615 prev.addOrUpdateProperties = prev.addOrUpdateProperties.filter((prop) => !removedProperties.has(prop.key)); 3616 } 3617 if (prev.removeAllProperties || next.removeAllProperties) merged.removeAllProperties = true; 3618 if (prev.removeProperties || next.removeProperties) merged.removeProperties = [...prev.removeProperties || [], ...next.removeProperties || []]; 3619 if (prev.addOrUpdateProperties || next.addOrUpdateProperties) merged.addOrUpdateProperties = [...prev.addOrUpdateProperties || [], ...next.addOrUpdateProperties || []]; 3620 if (prev.newGeometry || next.newGeometry) merged.newGeometry = next.newGeometry || prev.newGeometry; 3621 return merged; 3622} 3623/** 3624* @internal 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`. 3627*/ 3628function diffToHashed(diff, promoteId) { 3629 if (!diff) return {}; 3630 const hashed = {}; 3631 hashed.removeAll = diff.removeAll; 3632 hashed.remove = new Set(diff.remove || []); 3633 hashed.add = new Map(diff.add?.map((feature) => [getFeatureId(feature, promoteId), feature])); 3634 hashed.update = new Map(diff.update?.map((update) => [update.id, update])); 3635 return hashed; 3636} 3637/** 3638* @internal 3639* Convert a hashed GeoJSONSourceDiff back to the array-based representation 3640*/ 3641function hashedToDiff(hashed) { 3642 const diff = {}; 3643 if (hashed.removeAll) diff.removeAll = hashed.removeAll; 3644 if (hashed.remove) diff.remove = Array.from(hashed.remove); 3645 if (hashed.add) diff.add = Array.from(hashed.add.values()); 3646 if (hashed.update) diff.update = Array.from(hashed.update.values()); 3647 return diff; 3648} 3649//#endregion 3650//#region src/util/geojson_bounds.ts 3651function extractCoordinates(coords) { 3652 if (!coords || coords.length === 0) return []; 3653 if (typeof coords[0] === "number") return [coords]; 3654 return coords.flatMap((c) => extractCoordinates(c)); 3655} 3656function getCoordinatesFromGeometry(geometry) { 3657 if (geometry.type === "GeometryCollection") return geometry.geometries.flatMap((g) => getCoordinatesFromGeometry(g)); 3658 return extractCoordinates(geometry.coordinates); 3659} 3660/** 3661* Calculates the bounding box of GeoJSON data. 3662* @param data - The GeoJSON data to calculate bounds for 3663* @returns The bounding box of the GeoJSON data 3664*/ 3665function getGeoJSONBounds(data) { 3666 const bounds = new LngLatBounds(); 3667 let coordinates; 3668 switch (data.type) { 3669 case "FeatureCollection": 3670 coordinates = data.features.flatMap((f) => getCoordinatesFromGeometry(f.geometry)); 3671 break; 3672 case "Feature": 3673 coordinates = getCoordinatesFromGeometry(data.geometry); 3674 break; 3675 default: coordinates = getCoordinatesFromGeometry(data); 3676 } 3677 if (coordinates.length === 0) return bounds; 3678 for (const coordinate of coordinates) { 3679 const [lng, lat] = coordinate; 3680 bounds.extend([lng, lat]); 3681 } 3682 return bounds; 3683} 3684//#endregion 3685//#region src/tile/tile_id_to_lng_lat_bounds.ts 3686function tileIdToLngLatBounds({ x, y, z }, buffer = 0) { 3687 const lngMin = lngFromMercatorX((x - buffer) / Math.pow(2, z)); 3688 const latMin = latFromMercatorY((y + 1 + buffer) / Math.pow(2, z)); 3689 const lngMax = lngFromMercatorX((x + 1 + buffer) / Math.pow(2, z)); 3690 const latMax = latFromMercatorY((y - buffer) / Math.pow(2, z)); 3691 return new LngLatBounds([lngMin, latMin], [lngMax, latMax]); 3692} 3693//#endregion 3694//#region src/source/geojson_source.ts 3695/** 3696* A source containing GeoJSON. 3697* (See the [Style Specification](https://maplibre.org/maplibre-style-spec/#sources-geojson) for detailed documentation of options.) 3698* 3699* GeoJSON is tiled internally for rendering. Features exposed from rendered tiles and related events come from vector-tile data, so GeoJSON foreign members that cannot be represented by the vector-tile format are not preserved there. Keep that data separately, or map it to supported feature properties, if you need it after tiling. 3700* 3701* @group Sources 3702* 3703* @example 3704* ```ts 3705* map.addSource('some id', { 3706* type: 'geojson', 3707* data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_ports.geojson' 3708* }); 3709* ``` 3710* 3711* @example 3712* ```ts 3713* map.addSource('some id', { 3714* type: 'geojson', 3715* data: { 3716* "type": "FeatureCollection", 3717* "features": [{ 3718* "type": "Feature", 3719* "properties": {}, 3720* "geometry": { 3721* "type": "Point", 3722* "coordinates": [ 3723* -76.53063297271729, 3724* 39.18174077994108 3725* ] 3726* } 3727* }] 3728* } 3729* }); 3730* ``` 3731* 3732* @example 3733* ```ts 3734* map.getSource('some id').setData({ 3735* "type": "FeatureCollection", 3736* "features": [{ 3737* "type": "Feature", 3738* "properties": { "name": "Null Island" }, 3739* "geometry": { 3740* "type": "Point", 3741* "coordinates": [ 0, 0 ] 3742* } 3743* }] 3744* }); 3745* ``` 3746* @see [Draw GeoJSON points](https://maplibre.org/maplibre-gl-js/docs/examples/draw-geojson-points/) 3747* @see [Add a GeoJSON line](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-geojson-line/) 3748* @see [Create a heatmap from points](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-heatmap-layer/) 3749* @see [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/) 3750*/ 3751var GeoJSONSource = class extends Evented { 3752 /** @internal */ 3753 constructor(id, options, dispatcher, eventedParent) { 3754 super(); 3755 this.id = id; 3756 this.type = "geojson"; 3757 this.minzoom = 0; 3758 this.maxzoom = 18; 3759 this.tileSize = 512; 3760 this.isTileClipped = true; 3761 this.reparseOverscaled = true; 3762 this._removed = false; 3763 this._isUpdatingWorker = false; 3764 this._pendingWorkerUpdate = { data: options.data }; 3765 this.actorPromise = dispatcher.getActor(); 3766 this.setEventedParent(eventedParent); 3767 this._data = typeof options.data === "string" ? { url: options.data } : { geojson: options.data }; 3768 this._options = extend({}, options); 3769 this._collectResourceTiming = options.collectResourceTiming; 3770 if (options.maxzoom !== void 0) this.maxzoom = options.maxzoom; 3771 if (options.type) this.type = options.type; 3772 if (options.attribution) this.attribution = options.attribution; 3773 this.promoteId = options.promoteId; 3774 if (options.clusterMaxZoom !== void 0 && this.maxzoom <= options.clusterMaxZoom) warnOnce(`The maxzoom value "${this.maxzoom}" is expected to be greater than the clusterMaxZoom value "${options.clusterMaxZoom}".`); 3775 this.workerOptions = extend({ 3776 source: this.id, 3777 geojsonVtOptions: { 3778 buffer: this._pixelsToTileUnits(options.buffer !== void 0 ? options.buffer : 128), 3779 tolerance: this._pixelsToTileUnits(options.tolerance !== void 0 ? options.tolerance : .375), 3780 extent: EXTENT, 3781 maxZoom: this.maxzoom, 3782 lineMetrics: options.lineMetrics || false, 3783 generateId: options.generateId || false, 3784 promoteId: this._promoteIdKey, 3785 cluster: options.cluster || false, 3786 clusterOptions: { 3787 maxZoom: this._getClusterMaxZoom(options.clusterMaxZoom), 3788 minPoints: Math.max(2, options.clusterMinPoints || 2), 3789 extent: EXTENT, 3790 radius: this._pixelsToTileUnits(options.clusterRadius || 50), 3791 log: false, 3792 generateId: options.generateId || false 3793 } 3794 }, 3795 clusterProperties: options.clusterProperties, 3796 filter: options.filter 3797 }, options.workerOptions); 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 } 3803 _hasPendingWorkerUpdate() { 3804 return this._pendingWorkerUpdate.data !== void 0 || this._pendingWorkerUpdate.diff !== void 0 || this._pendingWorkerUpdate.updateCluster; 3805 } 3806 _pixelsToTileUnits(pixelValue) { 3807 return pixelValue * (EXTENT / this.tileSize); 3808 } 3809 _tileUnitsToPixels(tileUnitValue) { 3810 return tileUnitValue / (EXTENT / this.tileSize); 3811 } 3812 _getClusterMaxZoom(clusterMaxZoom) { 3813 const effectiveClusterMaxZoom = clusterMaxZoom ? Math.round(clusterMaxZoom) : this.maxzoom - 1; 3814 if (!(Number.isInteger(clusterMaxZoom) || clusterMaxZoom === void 0)) warnOnce(`Integer expected for option 'clusterMaxZoom': provided value "${clusterMaxZoom}" rounded to "${effectiveClusterMaxZoom}"`); 3815 return effectiveClusterMaxZoom; 3816 } 3817 async load() { 3818 await this._updateWorkerData(); 3819 } 3820 onAdd(map) { 3821 this.map = map; 3822 this.load(); 3823 } 3824 /** 3825 * Sets the GeoJSON data and re-renders the map. 3826 * 3827 * @param data - A GeoJSON data object or a URL to one. The latter is preferable in the case of large GeoJSON files. 3828 */ 3829 setData(data) { 3830 this._data = typeof data === "string" ? { url: data } : { geojson: data }; 3831 this._pendingWorkerUpdate = { data }; 3832 return this._updateWorkerData(); 3833 } 3834 /** 3835 * Updates the source's GeoJSON, and re-renders the map. 3836 * 3837 * For sources with lots of features, this method can be used to make updates more quickly. 3838 * 3839 * This approach requires unique IDs for every feature in the source. The IDs can either be specified on the feature, 3840 * or by using the promoteId option to specify which property should be used as the ID. 3841 * 3842 * It is an error to call updateData on a source that did not have unique IDs for each of its features already. 3843 * 3844 * Updates are applied on a best-effort basis, updating an ID that does not exist will not result in an error. 3845 * 3846 * @param diff - The changes that need to be applied. 3847 */ 3848 updateData(diff) { 3849 this._pendingWorkerUpdate.diff = mergeSourceDiffs(this._pendingWorkerUpdate.diff, diff, this._promoteIdKey); 3850 return this._updateWorkerData(); 3851 } 3852 /** 3853 * Allows to get the source's actual GeoJSON data. 3854 * 3855 * @returns a promise which resolves to the source's actual GeoJSON data 3856 */ 3857 async getData() { 3858 if (this._data.url) await this.once("data"); 3859 if (this._data.geojson) return this._data.geojson; 3860 return { 3861 type: "FeatureCollection", 3862 features: Array.from(this._data.updateable.values()) 3863 }; 3864 } 3865 /** 3866 * Allows getting the source's boundaries. 3867 * If there's a problem with the source's data, it will return an empty {@link LngLatBounds}. 3868 * @returns a promise which resolves to the source's boundaries 3869 */ 3870 async getBounds() { 3871 return getGeoJSONBounds(await this.getData()); 3872 } 3873 /** 3874 * To disable/enable clustering on the source options 3875 * @param options - The options to set 3876 * @example 3877 * ```ts 3878 * map.getSource('some id').setClusterOptions({cluster: false}); 3879 * map.getSource('some id').setClusterOptions({cluster: false, clusterRadius: 50, clusterMaxZoom: 14}); 3880 * ``` 3881 */ 3882 setClusterOptions(options) { 3883 this.workerOptions.geojsonVtOptions.cluster = options.cluster; 3884 if (options.clusterRadius !== void 0) this.workerOptions.geojsonVtOptions.clusterOptions.radius = this._pixelsToTileUnits(options.clusterRadius); 3885 if (options.clusterMaxZoom !== void 0) this.workerOptions.geojsonVtOptions.clusterOptions.maxZoom = this._getClusterMaxZoom(options.clusterMaxZoom); 3886 this._pendingWorkerUpdate.updateCluster = true; 3887 return this._updateWorkerData(); 3888 } 3889 /** 3890 * Gets the cluster options currently configured on the source. 3891 * The returned values mirror the options accepted by `setClusterOptions`. 3892 * 3893 * @returns the source's current cluster options 3894 * @example 3895 * ```ts 3896 * const {cluster, clusterMaxZoom, clusterRadius} = map.getSource('some id').getClusterOptions(); 3897 * ``` 3898 */ 3899 getClusterOptions() { 3900 const { cluster, clusterOptions } = this.workerOptions.geojsonVtOptions; 3901 return { 3902 cluster, 3903 clusterMaxZoom: clusterOptions.maxZoom, 3904 clusterRadius: this._tileUnitsToPixels(clusterOptions.radius) 3905 }; 3906 } 3907 /** 3908 * For clustered sources, fetches the zoom at which the given cluster expands. 3909 * 3910 * @param clusterId - The value of the cluster's `cluster_id` property. 3911 * @returns a promise that is resolved with the zoom number 3912 */ 3913 async getClusterExpansionZoom(clusterId) { 3914 return (await this.actorPromise).sendAsync({ 3915 type: "GCEZ", 3916 data: { 3917 type: this.type, 3918 clusterId, 3919 source: this.id 3920 } 3921 }); 3922 } 3923 /** 3924 * For clustered sources, fetches the children of the given cluster on the next zoom level (as an array of GeoJSON features). 3925 * 3926 * @param clusterId - The value of the cluster's `cluster_id` property. 3927 * @returns a promise that is resolved when the features are retrieved 3928 */ 3929 async getClusterChildren(clusterId) { 3930 return (await this.actorPromise).sendAsync({ 3931 type: "GCC", 3932 data: { 3933 type: this.type, 3934 clusterId, 3935 source: this.id 3936 } 3937 }); 3938 } 3939 /** 3940 * For clustered sources, fetches the original points that belong to the cluster (as an array of GeoJSON features). 3941 * 3942 * @param clusterId - The value of the cluster's `cluster_id` property. 3943 * @param limit - The maximum number of features to return. 3944 * @param offset - The number of features to skip (e.g. for pagination). 3945 * @returns a promise that is resolved when the features are retrieved 3946 * @example 3947 * Retrieve cluster leaves on click 3948 * ```ts 3949 * map.on('click', 'clusters', (e) => { 3950 * let features = map.queryRenderedFeatures(e.point, { 3951 * layers: ['clusters'] 3952 * }); 3953 * 3954 * let clusterId = features[0].properties.cluster_id; 3955 * let pointCount = features[0].properties.point_count; 3956 * let clusterSource = map.getSource('clusters'); 3957 * 3958 * const features = await clusterSource.getClusterLeaves(clusterId, pointCount); 3959 * // Print cluster leaves in the console 3960 * console.log('Cluster leaves:', features); 3961 * }); 3962 * ``` 3963 */ 3964 async getClusterLeaves(clusterId, limit, offset) { 3965 return (await this.actorPromise).sendAsync({ 3966 type: "GCL", 3967 data: { 3968 type: this.type, 3969 source: this.id, 3970 clusterId, 3971 limit, 3972 offset 3973 } 3974 }); 3975 } 3976 /** 3977 * Responsible for invoking WorkerSource's geojson.loadData target, which 3978 * handles loading the geojson data and preparing to serve it up as tiles, 3979 * using geojson-vt or supercluster as appropriate. 3980 */ 3981 async _updateWorkerData() { 3982 if (this._isUpdatingWorker) return this._updatePromise; 3983 if (!this._hasPendingWorkerUpdate()) { 3984 warnOnce(`No pending worker updates for GeoJSONSource ${this.id}.`); 3985 return; 3986 } 3987 const { data, diff, updateCluster } = this._pendingWorkerUpdate; 3988 const params = this._getLoadGeoJSONParameters(data, diff, updateCluster); 3989 if (data !== void 0) this._pendingWorkerUpdate.data = void 0; 3990 else if (diff) this._pendingWorkerUpdate.diff = void 0; 3991 else if (updateCluster) this._pendingWorkerUpdate.updateCluster = void 0; 3992 this._updatePromise = this._dispatchWorkerUpdate(params); 3993 await this._updatePromise; 3994 } 3995 /** 3996 * Create the parameters object that will be sent to the worker and used to load GeoJSON. 3997 */ 3998 async _getLoadGeoJSONParameters(data, diff, updateCluster) { 3999 const params = extend({ 4000 type: this.type, 4001 source: this.id 4002 }, this.workerOptions); 4003 if (typeof data === "string") { 4004 params.request = await this.map._requestManager.transformRequest(browser.resolveURL(data), "Source"); 4005 params.request.collectResourceTiming = this._collectResourceTiming; 4006 return params; 4007 } 4008 if (data !== void 0) { 4009 params.data = data; 4010 return params; 4011 } 4012 if (diff) { 4013 params.dataDiff = diff; 4014 return params; 4015 } 4016 if (updateCluster) { 4017 params.updateCluster = true; 4018 return params; 4019 } 4020 } 4021 /** 4022 * Send the worker update data from the main thread to the worker 4023 */ 4024 async _dispatchWorkerUpdate(optionsPromise) { 4025 this._isUpdatingWorker = true; 4026 this.fire(new MapSourceDataEvent("dataloading")); 4027 try { 4028 const options = await optionsPromise; 4029 const result = await (await this.actorPromise).sendAsync({ 4030 type: "LD", 4031 data: options 4032 }); 4033 this._isUpdatingWorker = false; 4034 if (this._removed || result.abandoned) { 4035 this.fire(new MapSourceDataEvent("dataabort")); 4036 return; 4037 } 4038 if (result.data) this._data = { geojson: result.data }; 4039 const affectedGeometries = this._applyDiffToSource(options.dataDiff); 4040 const shouldReloadTileOptions = this._getShouldReloadTileOptions(affectedGeometries); 4041 const eventData = {}; 4042 this._applyResourceTiming(eventData, result); 4043 this.fire(new MapSourceDataEvent("data", { 4044 ...eventData, 4045 sourceDataType: "metadata" 4046 })); 4047 this.fire(new MapSourceDataEvent("data", { 4048 ...eventData, 4049 sourceDataType: "content", 4050 shouldReloadTileOptions 4051 })); 4052 } catch (err) { 4053 this._isUpdatingWorker = false; 4054 if (this._removed) { 4055 this.fire(new MapSourceDataEvent("dataabort")); 4056 return; 4057 } 4058 this.fire(new ErrorEvent(ensureError(err))); 4059 } finally { 4060 if (this._hasPendingWorkerUpdate()) await this._updateWorkerData(); 4061 } 4062 } 4063 /** 4064 * Apply resource timing data to the event object. 4065 */ 4066 _applyResourceTiming(eventData, result) { 4067 if (!this._collectResourceTiming) return; 4068 const timingData = result.resourceTiming?.[this.id]; 4069 if (!timingData) return; 4070 const resourceTiming = timingData.slice(0); 4071 if (!resourceTiming?.length) return; 4072 extend(eventData, { resourceTiming }); 4073 } 4074 /** 4075 * Apply a diff to this source's data and return the affected feature geometries. 4076 * @param diff - The {@link GeoJSONSourceDiff} to apply. 4077 * @returns The affected geometries, or undefined if the diff is not applicable or all geometries are affected. 4078 */ 4079 _applyDiffToSource(diff) { 4080 if (!diff) return; 4081 const promoteId = this._promoteIdKey; 4082 if (!this._data.url && !this._data.updateable) { 4083 const updateable = toUpdateable(this._data.geojson, promoteId); 4084 if (!updateable) throw new Error(`GeoJSONSource "${this.id}": GeoJSON data is not compatible with updateData`); 4085 this._data = { updateable }; 4086 } 4087 if (!this._data.updateable) return; 4088 const affectedGeometries = applySourceDiff(this._data.updateable, diff, promoteId); 4089 if (diff.removeAll || this._options.cluster) return; 4090 return affectedGeometries; 4091 } 4092 /** 4093 * Get options for use in determining whether to reload a tile based on the modified features. 4094 * @param affectedGeometries - The feature geometries affected by the update. 4095 * @returns A {@link GeoJSONSourceShouldReloadTileOptions} object which contains an array of affected bounds caused by the update. 4096 */ 4097 _getShouldReloadTileOptions(affectedGeometries) { 4098 if (!affectedGeometries) return void 0; 4099 return { affectedBounds: affectedGeometries.filter(Boolean).map((g) => getGeoJSONBounds(g)) }; 4100 } 4101 /** 4102 * Determine whether a tile should be reloaded based on a set of options associated with a {@link MapSourceDataChangedEvent}. 4103 * @internal 4104 */ 4105 shouldReloadTile(tile, { affectedBounds }) { 4106 if (tile.state === "loading") return true; 4107 if (tile.state === "unloaded") return false; 4108 const { buffer, extent } = this.workerOptions.geojsonVtOptions; 4109 const tileBounds = tileIdToLngLatBounds(tile.tileID.canonical, buffer / extent); 4110 for (const bounds of affectedBounds) if (tileBounds.intersects(bounds)) return true; 4111 return false; 4112 } 4113 loaded() { 4114 return !this._isUpdatingWorker && !this._hasPendingWorkerUpdate(); 4115 } 4116 async loadTile(tile) { 4117 const message = !tile.actor ? "LT" : "RT"; 4118 tile.actor = await this.actorPromise; 4119 const params = { 4120 type: this.type, 4121 uid: tile.uid, 4122 tileID: tile.tileID, 4123 zoom: tile.tileID.overscaledZ, 4124 maxZoom: this.maxzoom, 4125 tileSize: this.tileSize, 4126 source: this.id, 4127 pixelRatio: this.map.getPixelRatio(), 4128 showCollisionBoxes: this.map.showCollisionBoxes, 4129 promoteId: this.promoteId, 4130 subdivisionGranularity: this.map.style.projection.subdivisionGranularity 4131 }; 4132 tile.abortController = new AbortController(); 4133 try { 4134 const data = await (await this.actorPromise).sendAsync({ 4135 type: message, 4136 data: params 4137 }, tile.abortController); 4138 delete tile.abortController; 4139 tile.unloadVectorData(); 4140 if (!tile.aborted) tile.loadVectorData(data, this.map.painter, message === "RT"); 4141 } catch (err) { 4142 delete tile.abortController; 4143 if (tile.aborted || isAbortError(err)) return; 4144 throw err; 4145 } 4146 } 4147 async abortTile(tile) { 4148 if (tile.abortController) { 4149 tile.abortController.abort(); 4150 delete tile.abortController; 4151 } 4152 tile.aborted = true; 4153 } 4154 async unloadTile(tile) { 4155 tile.unloadVectorData(); 4156 await (await this.actorPromise).sendAsync({ 4157 type: "RMT", 4158 data: { 4159 uid: tile.uid, 4160 type: this.type, 4161 source: this.id 4162 } 4163 }); 4164 } 4165 onRemove() { 4166 this._removed = true; 4167 this.actorPromise.then((actor) => actor.sendAsync({ 4168 type: "RS", 4169 data: { 4170 type: this.type, 4171 source: this.id 4172 } 4173 })); 4174 } 4175 serialize() { 4176 return extend({}, this._options, { 4177 type: this.type, 4178 data: this._data.updateable ? { 4179 type: "FeatureCollection", 4180 features: Array.from(this._data.updateable.values()) 4181 } : this._data.url || this._data.geojson 4182 }); 4183 } 4184 hasTransition() { 4185 return false; 4186 } 4187}; 4188//#endregion 4189//#region src/webgl/program/raster_program.ts 4190/** The bilinear warp, which is also the affine one that a rectangle or a parallelogram needs. */ 4191const bilinearImageWarp = [ 4192 0, 4193 0, 4194 1 4195]; 4196const rasterUniforms = (context, locations) => ({ 4197 "u_tl_parent": new Uniform2f(context, locations.u_tl_parent), 4198 "u_scale_parent": new Uniform1f(context, locations.u_scale_parent), 4199 "u_buffer_scale": new Uniform1f(context, locations.u_buffer_scale), 4200 "u_image_warp": new Uniform3f(context, locations.u_image_warp), 4201 "u_fade_t": new Uniform1f(context, locations.u_fade_t), 4202 "u_opacity": new Uniform1f(context, locations.u_opacity), 4203 "u_image0": new Uniform1i(context, locations.u_image0), 4204 "u_image1": new Uniform1i(context, locations.u_image1), 4205 "u_brightness_low": new Uniform1f(context, locations.u_brightness_low), 4206 "u_brightness_high": new Uniform1f(context, locations.u_brightness_high), 4207 "u_saturation_factor": new Uniform1f(context, locations.u_saturation_factor), 4208 "u_contrast_factor": new Uniform1f(context, locations.u_contrast_factor), 4209 "u_spin_weights": new Uniform3f(context, locations.u_spin_weights), 4210 "u_coords_top": new Uniform4f(context, locations.u_coords_top), 4211 "u_coords_bottom": new Uniform4f(context, locations.u_coords_bottom) 4212}); 4213const rasterUniformValues = (parentTL, parentScaleBy, fade, layer, cornerCoords, imageWarp) => ({ 4214 "u_tl_parent": parentTL, 4215 "u_scale_parent": parentScaleBy, 4216 "u_buffer_scale": 1, 4217 "u_image_warp": imageWarp, 4218 "u_fade_t": fade.mix, 4219 "u_opacity": fade.opacity * layer.paint.get("raster-opacity"), 4220 "u_image0": 0, 4221 "u_image1": 1, 4222 "u_brightness_low": layer.paint.get("raster-brightness-min"), 4223 "u_brightness_high": layer.paint.get("raster-brightness-max"), 4224 "u_saturation_factor": saturationFactor(layer.paint.get("raster-saturation")), 4225 "u_contrast_factor": contrastFactor(layer.paint.get("raster-contrast")), 4226 "u_spin_weights": spinWeights(layer.paint.get("raster-hue-rotate")), 4227 "u_coords_top": [ 4228 cornerCoords[0].x, 4229 cornerCoords[0].y, 4230 cornerCoords[1].x, 4231 cornerCoords[1].y 4232 ], 4233 "u_coords_bottom": [ 4234 cornerCoords[3].x, 4235 cornerCoords[3].y, 4236 cornerCoords[2].x, 4237 cornerCoords[2].y 4238 ] 4239}); 4240function spinWeights(angle) { 4241 angle *= Math.PI / 180; 4242 const s = Math.sin(angle); 4243 const c = Math.cos(angle); 4244 return [ 4245 (2 * c + 1) / 3, 4246 (-Math.sqrt(3) * s - c + 1) / 3, 4247 (Math.sqrt(3) * s - c + 1) / 3 4248 ]; 4249} 4250function contrastFactor(contrast) { 4251 return contrast > 0 ? 1 / (1 - contrast) : 1 + contrast; 4252} 4253function saturationFactor(saturation) { 4254 return saturation > 0 ? 1 - 1 / (1.001 - saturation) : -saturation; 4255} 4256//#endregion 4257//#region src/render/mesh.ts 4258var Mesh = class { 4259 constructor(vertexBuffer, indexBuffer, segments) { 4260 this.vertexBuffer = vertexBuffer; 4261 this.indexBuffer = indexBuffer; 4262 this.segments = segments; 4263 } 4264 destroy() { 4265 this.vertexBuffer.destroy(); 4266 this.indexBuffer.destroy(); 4267 this.segments.destroy(); 4268 this.vertexBuffer = null; 4269 this.indexBuffer = null; 4270 this.segments = null; 4271 } 4272}; 4273//#endregion 4274//#region src/data/pos_attributes.ts 4275const posAttributes = createLayout([{ 4276 name: "a_pos", 4277 type: "Int16", 4278 components: 2 4279}]); 4280//#endregion 4281//#region src/util/create_tile_mesh.ts 4282/** 4283* The size of border region for stencil masks, in internal tile coordinates. 4284* Used for globe rendering. 4285*/ 4286const EXTENT_STENCIL_BORDER = EXTENT / 128; 4287/** 4288* @internal 4289* Creates a mesh of a quad that covers the entire tile (covering positions in range 0..EXTENT), 4290* is optionally subdivided into finer quads, optionally includes a border 4291* and optionally extends to the north and/or special pole vertices. 4292* Also allocates and populates WebGL buffers for the mesh. 4293* Forces 16 bit indices that are used throughout MapLibre. 4294* @param context - The WebGL context wrapper. 4295* @param options - Specify options for tile mesh creation such as granularity or border. 4296* @returns The mesh vertices and indices, already allocated and uploaded into WebGL buffers. 4297*/ 4298function createTileMeshWithBuffers(context, options) { 4299 const tileMesh = createTileMesh(options, "16bit"); 4300 const vertices = PosArray.deserialize({ 4301 arrayBuffer: tileMesh.vertices, 4302 length: tileMesh.vertices.byteLength / 2 / 2 4303 }); 4304 const indices = TriangleIndexArray.deserialize({ 4305 arrayBuffer: tileMesh.indices, 4306 length: tileMesh.indices.byteLength / 2 / 3 4307 }); 4308 return new Mesh(context.createVertexBuffer(vertices, posAttributes.members), context.createIndexBuffer(indices), SegmentVector.simpleSegment(0, 0, vertices.length, indices.length)); 4309} 4310/** 4311* Creates a mesh of a quad that covers the entire tile (covering positions in range 0..EXTENT), 4312* is optionally subdivided into finer quads, optionally includes a border 4313* and optionally extends to the north and/or special pole vertices. 4314* Additionally the resulting mesh indices type can be specified using `forceIndicesSize`. 4315* @example 4316* ``` 4317* // Creating a mesh for a tile that can be used for raster layers, hillshade, etc. 4318* const meshBuffers = createTileMesh({ 4319* granularity: map.style.projection.subdivisionGranularity.tile.getGranularityForZoomLevel(tileID.z), 4320* generateBorders: true, 4321* extendToNorthPole: tileID.y === 0, 4322* extendToSouthPole: tileID.y === (1 << tileID.z) - 1, 4323* }, '16bit'); 4324* ``` 4325* @see [Add a custom layer with tiles to a globe](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-custom-layer-with-tiles-to-a-globe/) 4326* @param options - Specify options for tile mesh creation such as granularity or border. 4327* @param forceIndicesSize - Specifies what indices type to use. The values '32bit' and '16bit' force their respective indices size. If undefined, the mesh may use either size, and will pick 16 bit indices if possible. If '16bit' is specified and the mesh exceeds 65536 vertices, an exception is thrown. 4328* @returns Typed arrays of the mesh vertices and indices. 4329*/ 4330function createTileMesh(options, forceIndicesSize) { 4331 const granularity = options.granularity !== void 0 ? Math.max(options.granularity, 1) : 1; 4332 const quadsPerAxisX = granularity + (options.generateBorders ? 2 : 0); 4333 const quadsPerAxisY = granularity + (options.extendToNorthPole || options.generateBorders ? 1 : 0) + (options.extendToSouthPole || options.generateBorders ? 1 : 0); 4334 const verticesPerAxisX = quadsPerAxisX + 1; 4335 const verticesPerAxisY = quadsPerAxisY + 1; 4336 const offsetX = options.generateBorders ? -1 : 0; 4337 const offsetY = options.generateBorders || options.extendToNorthPole ? -1 : 0; 4338 const endX = granularity + (options.generateBorders ? 1 : 0); 4339 const endY = granularity + (options.generateBorders || options.extendToSouthPole ? 1 : 0); 4340 const vertexCount = verticesPerAxisX * verticesPerAxisY; 4341 const indexCount = quadsPerAxisX * quadsPerAxisY * 6; 4342 const overflows16bitIndices = verticesPerAxisX * verticesPerAxisY > 65536; 4343 if (overflows16bitIndices && forceIndicesSize === "16bit") throw new Error("Granularity is too large and meshes would not fit inside 16 bit vertex indices."); 4344 const use32bitIndices = overflows16bitIndices || forceIndicesSize === "32bit"; 4345 const vertices = new Int16Array(vertexCount * 2); 4346 let vertexId = 0; 4347 for (let y = offsetY; y <= endY; y++) for (let x = offsetX; x <= endX; x++) { 4348 let vx = x / granularity * EXTENT; 4349 if (x === -1) vx = -EXTENT_STENCIL_BORDER; 4350 if (x === granularity + 1) vx = EXTENT + EXTENT_STENCIL_BORDER; 4351 let vy = y / granularity * EXTENT; 4352 if (y === -1) vy = options.extendToNorthPole ? NORTH_POLE_Y : -EXTENT_STENCIL_BORDER; 4353 if (y === granularity + 1) vy = options.extendToSouthPole ? SOUTH_POLE_Y : EXTENT + EXTENT_STENCIL_BORDER; 4354 vertices[vertexId++] = vx; 4355 vertices[vertexId++] = vy; 4356 } 4357 const indices = use32bitIndices ? new Uint32Array(indexCount) : new Uint16Array(indexCount); 4358 let indexId = 0; 4359 for (let y = 0; y < quadsPerAxisY; y++) for (let x = 0; x < quadsPerAxisX; x++) { 4360 const v0 = x + y * verticesPerAxisX; 4361 const v1 = x + 1 + y * verticesPerAxisX; 4362 const v2 = x + (y + 1) * verticesPerAxisX; 4363 const v3 = x + 1 + (y + 1) * verticesPerAxisX; 4364 indices[indexId++] = v0; 4365 indices[indexId++] = v2; 4366 indices[indexId++] = v1; 4367 indices[indexId++] = v1; 4368 indices[indexId++] = v2; 4369 indices[indexId++] = v3; 4370 } 4371 return { 4372 vertices: vertices.buffer.slice(0), 4373 indices: indices.buffer.slice(0), 4374 uses32bitIndices: use32bitIndices 4375 }; 4376} 4377//#endregion 4378//#region src/source/image_source.ts 4379/** 4380* How many grid cells per axis the mesh of a subdivided quad has. Within a cell the bilinear warp 4381* is still approximated by one mapping per triangle, so the error falls off with the cell size. 4382*/ 4383const SUBDIVIDED_QUAD_GRANULARITY = 16; 4384/** 4385* How much a warp may foreshorten the image, as the ratio between the largest and the smallest 4386* homogeneous denominator over the quad, while staying purely projective. Ordinary oblique imagery 4387* stays well below it. 4388*/ 4389const PROJECTIVE_FORESHORTENING = 4; 4390/** 4391* The foreshortening at which a warp is taken to be purely bilinear. 4392* 4393* The denominator vanishes on the warp's vanishing line, and that line touches the quad exactly when 4394* the quad degenerates into a triangle, so foreshortening grows without bound as a corner approaches 4395* the diagonal between its neighbours. Left alone, such a quad keeps a valid but wildly lopsided 4396* warp, squeezing nearly the whole image into a sliver along one edge and magnifying a handful of 4397* texels over the rest of the quad. 4398*/ 4399const BILINEAR_FORESHORTENING = 512; 4400/** 4401* A data source containing an image. 4402* (See the [Style Specification](https://maplibre.org/maplibre-style-spec/#sources-image) for detailed documentation of options.) 4403* 4404* @group Sources 4405* 4406* @example 4407* ```ts 4408* // add to map 4409* map.addSource('some id', { 4410* type: 'image', 4411* url: 'https://www.maplibre.org/images/foo.png', 4412* coordinates: [ 4413* [-76.54, 39.18], 4414* [-76.52, 39.18], 4415* [-76.52, 39.17], 4416* [-76.54, 39.17] 4417* ] 4418* }); 4419* 4420* // update coordinates 4421* let mySource = map.getSource('some id'); 4422* mySource.setCoordinates([ 4423* [-76.54335737228394, 39.18579907229748], 4424* [-76.52803659439087, 39.1838364847587], 4425* [-76.5295386314392, 39.17683392507606], 4426* [-76.54520273208618, 39.17876344106642] 4427* ]); 4428* 4429* // update url and coordinates simultaneously 4430* mySource.updateImage({ 4431* url: 'https://www.maplibre.org/images/bar.png', 4432* coordinates: [ 4433* [-76.54335737228394, 39.18579907229748], 4434* [-76.52803659439087, 39.1838364847587], 4435* [-76.5295386314392, 39.17683392507606], 4436* [-76.54520273208618, 39.17876344106642] 4437* ] 4438* }) 4439* 4440* // update with an already-decoded image (no network request) 4441* const bitmap = await createImageBitmap(myCanvas); 4442* mySource.updateImage({image: bitmap}); 4443* 4444* // create an empty source (no url), then feed it entirely via updateImage 4445* map.addSource('empty id', { 4446* type: 'image', 4447* coordinates: [ 4448* [-76.54, 39.18], 4449* [-76.52, 39.18], 4450* [-76.52, 39.17], 4451* [-76.54, 39.17] 4452* ] 4453* }); 4454* (map.getSource('empty id') as ImageSource).updateImage({image: bitmap}); 4455* 4456* map.removeSource('some id'); // remove 4457* ``` 4458*/ 4459var ImageSource = class extends Evented { 4460 /** @internal */ 4461 constructor(id, options, dispatcher, eventedParent) { 4462 super(); 4463 this.imageWarp = bilinearImageWarp; 4464 this.flippedWindingOrder = false; 4465 this._warp = "auto"; 4466 this._imageDirty = false; 4467 this._subdividedQuad = false; 4468 this._subdividedMesh = null; 4469 this.id = id; 4470 this.dispatcher = dispatcher; 4471 this.coordinates = options.coordinates; 4472 this.type = "image"; 4473 this.minzoom = 0; 4474 this.maxzoom = 22; 4475 this.tileSize = 512; 4476 this.tiles = {}; 4477 this._loaded = false; 4478 this.setEventedParent(eventedParent); 4479 this.options = options; 4480 } 4481 async load(newCoordinates) { 4482 if (this.options.url === void 0) { 4483 this._loaded = true; 4484 this._finishLoading(); 4485 return; 4486 } 4487 this._loaded = false; 4488 this.fire(new MapSourceDataEvent("dataloading")); 4489 this.url = this.options.url; 4490 this._abortController = new AbortController(); 4491 try { 4492 const image = await ImageRequest.transformAndGetImage(this.map._requestManager, this.url, "Image", this._abortController); 4493 this._abortController = null; 4494 this._loaded = true; 4495 if (image?.data) { 4496 this._setImage(image.data); 4497 if (newCoordinates) this.coordinates = newCoordinates; 4498 this._finishLoading(); 4499 } else this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Could not load image ${this.url}: the response is empty`))); 4500 } catch (err) { 4501 if (isAbortError(err)) return; 4502 this._abortController = null; 4503 this._loaded = true; 4504 this.fire(new ErrorEvent(ensureError(err))); 4505 } 4506 } 4507 loaded() { 4508 return this._loaded; 4509 } 4510 /** 4511 * Updates the image and, optionally, the coordinates. To avoid having the image flash after changing, 4512 * set the `raster-fade-duration` paint property on the raster layer to 0. 4513 * 4514 * Provide exactly one of `url` (to fetch a new image over the network) or `image` (an 4515 * already-decoded `HTMLImageElement`, `HTMLCanvasElement`, `ImageBitmap` or `ImageData` to 4516 * display directly, without a network request). 4517 * 4518 * @param options - The options object. 4519 */ 4520 updateImage(options) { 4521 if (this._abortController) { 4522 this._abortController.abort(); 4523 this._abortController = null; 4524 } 4525 if ("image" in options) { 4526 this._loaded = true; 4527 this._setImage(options.image); 4528 if (options.coordinates) this.coordinates = options.coordinates; 4529 this._finishLoading(); 4530 return this; 4531 } 4532 if (!options.url) return this; 4533 this.options.url = options.url; 4534 this.load(options.coordinates); 4535 return this; 4536 } 4537 /** Loaded tiles hold `this.texture`, so the wrapper has to outlive the images in it. */ 4538 _setImage(image) { 4539 this.image = image; 4540 this._imageDirty = true; 4541 } 4542 /** 4543 * @internal 4544 * The mesh the image is drawn with, or null to use the tile mesh of the current projection. 4545 * 4546 * The raster vertex shader evaluates {@link imageWarp} per vertex, so a pair of triangles only 4547 * samples it at the four corners. That is enough for the affine mapping of a parallelogram, and 4548 * for a purely projective mapping, whose straight lines survive linear interpolation across the 4549 * diagonal. Anything in between is neither, and would be textured as two independently warped 4550 * halves with a visible seam along that diagonal, so it needs the warp evaluated per grid cell. 4551 * 4552 * A projection that subdivides its own tile meshes already does this, so it keeps them. 4553 */ 4554 getMesh(context, projectionSubdividesTiles) { 4555 if (!this._subdividedQuad || projectionSubdividesTiles) return null; 4556 this._subdividedMesh ??= createTileMeshWithBuffers(context, { granularity: SUBDIVIDED_QUAD_GRANULARITY }); 4557 return this._subdividedMesh; 4558 } 4559 /** Teardown only: dropping the reference alone leaves the allocation to the GC. */ 4560 _disposeTexture() { 4561 this.texture?.destroy(); 4562 this.texture = null; 4563 } 4564 _finishLoading() { 4565 if (this.map) { 4566 this.setCoordinates(this.coordinates); 4567 this.fire(new MapSourceDataEvent("data", { sourceDataType: "metadata" })); 4568 } 4569 } 4570 onAdd(map) { 4571 this.map = map; 4572 this.load(); 4573 } 4574 onRemove() { 4575 if (this._abortController) { 4576 this._abortController.abort(); 4577 this._abortController = null; 4578 } 4579 this._disposeTexture(); 4580 this._subdividedMesh?.destroy(); 4581 this._subdividedMesh = null; 4582 this.image = null; 4583 this.tiles = {}; 4584 } 4585 /** 4586 * @experimental 4587 * Sets how the image is warped onto its coordinates and re-renders the map. 4588 * 4589 * This only has an effect while the coordinates do not form a rectangle, and it is not part of 4590 * the style specification, so it does not survive `map.setStyle`. 4591 * 4592 * @param warp - The warp to use, see {@link ImageSourceWarp}. 4593 * 4594 * @example 4595 * ```ts 4596 * // Keep the image pinned to its corners while the user drags them around. 4597 * map.getSource('some id').setWarp('flat'); 4598 * ``` 4599 */ 4600 setWarp(warp) { 4601 if (this._warp === warp) return this; 4602 this._warp = warp; 4603 if (this.tileCoords) this.setCoordinates(this.coordinates); 4604 return this; 4605 } 4606 /** 4607 * @experimental 4608 * Returns how the image is warped onto its coordinates. 4609 * 4610 * @returns The warp in use, see {@link ImageSourceWarp}. 4611 */ 4612 getWarp() { 4613 return this._warp; 4614 } 4615 /** 4616 * Sets the image's coordinates and re-renders the map. 4617 * 4618 * @param coordinates - Four geographical coordinates, 4619 * represented as arrays of longitude and latitude numbers, which define the corners of the image. 4620 * The coordinates start at the top left corner of the image and proceed in clockwise order. 4621 * They do not have to represent a rectangle. 4622 */ 4623 setCoordinates(coordinates) { 4624 this.coordinates = coordinates; 4625 const cornerCoords = coordinates.map(MercatorCoordinate.fromLngLat); 4626 this.tileID = getCoordinatesCenterTileID(cornerCoords); 4627 this.terrainTileRanges = this._getOverlappingTileRanges(cornerCoords); 4628 this.minzoom = this.maxzoom = this.tileID.z; 4629 this.tileCoords = cornerCoords.map((coord) => this.tileID.getTilePoint(coord)._round()); 4630 this.imageWarp = calculateImageWarp(this.tileCoords, this._warp); 4631 this._subdividedQuad = this.imageWarp[2] > 0 && !isParallelogram(this.tileCoords); 4632 this.flippedWindingOrder = hasWrongWindingOrder(this.tileCoords); 4633 this.fire(new MapSourceDataEvent("data", { sourceDataType: "content" })); 4634 return this; 4635 } 4636 prepare() { 4637 if (Object.keys(this.tiles).length === 0 || !this.image) return; 4638 const context = this.map.painter.context; 4639 const gl = context.gl; 4640 if (!this.texture) { 4641 this.texture = new Texture(context, this.image, gl.RGBA); 4642 this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 4643 } else if (this._imageDirty) { 4644 this.texture.update(this.image); 4645 this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 4646 } 4647 this._imageDirty = false; 4648 let newTilesLoaded = false; 4649 for (const w in this.tiles) { 4650 const tile = this.tiles[w]; 4651 if (tile.state !== "loaded") { 4652 tile.state = "loaded"; 4653 tile.texture = this.texture; 4654 newTilesLoaded = true; 4655 } 4656 } 4657 if (newTilesLoaded) this.fire(new MapSourceDataEvent("data", { 4658 sourceDataType: "idle", 4659 sourceId: this.id 4660 })); 4661 } 4662 async loadTile(tile) { 4663 if (this.tileID?.equals(tile.tileID.canonical)) { 4664 this.tiles[String(tile.tileID.wrap)] = tile; 4665 tile.buckets = {}; 4666 } else tile.state = "errored"; 4667 } 4668 serialize() { 4669 const spec = { 4670 type: "image", 4671 coordinates: this.coordinates 4672 }; 4673 if (this.options.url !== void 0) spec.url = this.options.url; 4674 return spec; 4675 } 4676 hasTransition() { 4677 return false; 4678 } 4679 /** 4680 * Given a list of coordinates, determine overlapping tile ranges for all zoom levels. 4681 * 4682 * @returns Overlapping tile ranges for all zoom levels. 4683 * @internal 4684 */ 4685 _getOverlappingTileRanges(coords) { 4686 const { minX, minY, maxX, maxY } = Bounds.fromPoints(coords); 4687 const ranges = {}; 4688 for (let z = 0; z <= 25; z++) { 4689 const tilesAtZoom = Math.pow(2, z); 4690 const minTileX = Math.floor(minX * tilesAtZoom); 4691 const minTileY = Math.floor(minY * tilesAtZoom); 4692 const maxTileX = Math.floor(maxX * tilesAtZoom); 4693 const maxTileY = Math.floor(maxY * tilesAtZoom); 4694 const minTileXWrapped = (minTileX % tilesAtZoom + tilesAtZoom) % tilesAtZoom; 4695 const maxTileXWrapped = maxTileX % tilesAtZoom; 4696 const minWrap = Math.floor(minTileX / tilesAtZoom); 4697 const maxWrap = Math.floor(maxTileX / tilesAtZoom); 4698 ranges[z] = { 4699 minWrap, 4700 maxWrap, 4701 minTileXWrapped, 4702 maxTileXWrapped, 4703 minTileY, 4704 maxTileY 4705 }; 4706 } 4707 return ranges; 4708 } 4709}; 4710/** 4711* Given a list of coordinates, get their center as a coordinate. 4712* 4713* @returns centerpoint 4714* @internal 4715*/ 4716function getCoordinatesCenterTileID(coords) { 4717 const bounds = Bounds.fromPoints(coords); 4718 const dx = bounds.width(); 4719 const dy = bounds.height(); 4720 const zoom = Math.max(0, Math.floor(-Math.log(Math.max(dx, dy)) / Math.LN2)); 4721 const tilesAtZoom = Math.pow(2, zoom); 4722 return new CanonicalTileID(zoom, Math.floor((bounds.minX + bounds.maxX) / 2 * tilesAtZoom), Math.floor((bounds.minY + bounds.maxY) / 2 * tilesAtZoom)); 4723} 4724function hasWrongWindingOrder(coords) { 4725 const e0x = coords[1].x - coords[0].x; 4726 const e0y = coords[1].y - coords[0].y; 4727 const e1x = coords[2].x - coords[0].x; 4728 return e0x * (coords[2].y - coords[0].y) - e0y * e1x < 0; 4729} 4730/** 4731* How the image is warped onto its coordinates, given the corners in tile space and what the user 4732* asked for. The projective warp is the perspective view of a plane, so it is the one a photograph 4733* wants; the bilinear warp is a rubber sheet pinned at the corners, and is the only one left once 4734* the corners stop describing a perspective view at all. 4735* 4736* Based on Paul S. Heckbert, "Fundamentals of Texture Mapping and Image 4737* Warping", UCB/CSD-89-516, 1989, section 2.2.3 and appendix A.2. 4738* 4739* @see https://www2.eecs.berkeley.edu/Pubs/TechRpts/1989/5504.html 4740* @see https://www.cs.cmu.edu/~ph/texfund/texfund.pdf 4741*/ 4742function calculateImageWarp(cornerCoords, warp) { 4743 if (warp === "flat" || isParallelogram(cornerCoords)) return bilinearImageWarp; 4744 const [topLeft, topRight, bottomRight, bottomLeft] = cornerCoords; 4745 const sumX = topLeft.x - topRight.x + bottomRight.x - bottomLeft.x; 4746 const sumY = topLeft.y - topRight.y + bottomRight.y - bottomLeft.y; 4747 const basis = [ 4748 topRight.x - bottomRight.x, 4749 topRight.y - bottomRight.y, 4750 bottomLeft.x - bottomRight.x, 4751 bottomLeft.y - bottomRight.y 4752 ]; 4753 const [rightX, rightY, downX, downY] = basis; 4754 const determinant$1 = determinant(basis); 4755 const perspectiveX = (sumX * downY - downX * sumY) / determinant$1; 4756 const perspectiveY = (rightX * sumY - sumX * rightY) / determinant$1; 4757 const denominators = [ 4758 1, 4759 1 + perspectiveX, 4760 1 + perspectiveX + perspectiveY, 4761 1 + perspectiveY 4762 ]; 4763 const foreshortening = Math.max(...denominators) / Math.min(...denominators); 4764 const blend = warp === "perspective" ? 0 : bilinearBlend(foreshortening); 4765 if (!(foreshortening >= 1 && foreshortening <= BILINEAR_FORESHORTENING) || blend >= 1) return bilinearImageWarp; 4766 return [ 4767 perspectiveX, 4768 perspectiveY, 4769 blend 4770 ]; 4771} 4772/** 4773* How far a warp of the given foreshortening is blended towards the bilinear one, ramping from 4774* purely projective at {@link PROJECTIVE_FORESHORTENING} to purely bilinear at 4775* {@link BILINEAR_FORESHORTENING}. Blending rather than switching keeps the image continuous as the 4776* quad is reshaped: the two warps only agree for a parallelogram, and diverge by roughly a tenth of 4777* the quad per unit of foreshortening, so a switch would visibly displace the image. 4778*/ 4779function bilinearBlend(foreshortening) { 4780 const ramp = (1 - PROJECTIVE_FORESHORTENING / foreshortening) / (1 - PROJECTIVE_FORESHORTENING / BILINEAR_FORESHORTENING); 4781 return Math.max(0, ramp); 4782} 4783function isParallelogram(cornerCoords) { 4784 const [topLeft, topRight, bottomRight, bottomLeft] = cornerCoords; 4785 return topLeft.x + bottomRight.x === topRight.x + bottomLeft.x && topLeft.y + bottomRight.y === topRight.y + bottomLeft.y; 4786} 4787//#endregion 4788//#region src/source/video_source.ts 4789/** 4790* A data source containing video. 4791* (See the [Style Specification](https://maplibre.org/maplibre-style-spec/#sources-video) for detailed documentation of options.) 4792* 4793* @group Sources 4794* 4795* @example 4796* ```ts 4797* // add to map 4798* map.addSource('some id', { 4799* type: 'video', 4800* url: [ 4801* 'https://www.mapbox.com/blog/assets/baltimore-smoke.mp4', 4802* 'https://www.mapbox.com/blog/assets/baltimore-smoke.webm' 4803* ], 4804* coordinates: [ 4805* [-76.54, 39.18], 4806* [-76.52, 39.18], 4807* [-76.52, 39.17], 4808* [-76.54, 39.17] 4809* ] 4810* }); 4811* 4812* // update 4813* let mySource = map.getSource('some id'); 4814* mySource.setCoordinates([ 4815* [-76.54335737228394, 39.18579907229748], 4816* [-76.52803659439087, 39.1838364847587], 4817* [-76.5295386314392, 39.17683392507606], 4818* [-76.54520273208618, 39.17876344106642] 4819* ]); 4820* 4821* map.removeSource('some id'); // remove 4822* ``` 4823* @see [Add a video](https://maplibre.org/maplibre-gl-js/docs/examples/video-on-a-map/) 4824* 4825* Note that when rendered as a raster layer, the layer's `raster-fade-duration` property will cause the video to fade in. 4826* This happens when playback is started, paused and resumed, or when the video's coordinates are updated. To avoid this behavior, 4827* set the layer's `raster-fade-duration` property to `0`. 4828*/ 4829var VideoSource = class extends ImageSource { 4830 constructor(id, options, dispatcher, eventedParent) { 4831 super(id, options, dispatcher, eventedParent); 4832 this._onPlayingHandler = () => { 4833 this.map?.triggerRepaint(); 4834 }; 4835 this.roundZoom = true; 4836 this.type = "video"; 4837 this.options = options; 4838 } 4839 async load() { 4840 this._loaded = false; 4841 const options = this.options; 4842 this.urls = []; 4843 for (const url of options.urls) this.urls.push((await this.map._requestManager.transformRequest(url, "Source")).url); 4844 try { 4845 const video = await getVideo(this.urls); 4846 this._loaded = true; 4847 if (!video) return; 4848 this.video = video; 4849 this.video.loop = true; 4850 this.video.addEventListener("playing", this._onPlayingHandler); 4851 if (this.map) this.video.play(); 4852 this._finishLoading(); 4853 } catch (err) { 4854 this.fire(new ErrorEvent(ensureError(err))); 4855 } 4856 } 4857 /** 4858 * Pauses the video. 4859 */ 4860 pause() { 4861 if (this.video) this.video.pause(); 4862 } 4863 /** 4864 * Plays the video. 4865 */ 4866 play() { 4867 if (this.video) this.video.play(); 4868 } 4869 /** 4870 * Sets playback to a timestamp, in seconds. 4871 */ 4872 seek(seconds) { 4873 if (this.video) { 4874 const seekableRange = this.video.seekable; 4875 if (seconds < seekableRange.start(0) || seconds > seekableRange.end(0)) this.fire(new ErrorEvent(new ValidationError(`sources.${this.id}`, null, `Playback for this video can be set only between the ${seekableRange.start(0)} and ${seekableRange.end(0)}-second mark.`))); 4876 else this.video.currentTime = seconds; 4877 } 4878 } 4879 /** 4880 * Returns the HTML `video` element. 4881 * 4882 * @returns The HTML `video` element. 4883 */ 4884 getVideo() { 4885 return this.video; 4886 } 4887 onAdd(map) { 4888 if (this.map) return; 4889 this.map = map; 4890 this.load(); 4891 if (this.video) { 4892 this.video.play(); 4893 this.setCoordinates(this.coordinates); 4894 } 4895 } 4896 onRemove() { 4897 super.onRemove(); 4898 if (this.video) { 4899 this.video.removeEventListener("playing", this._onPlayingHandler); 4900 this.video.pause(); 4901 } 4902 } 4903 /** 4904 * Sets the video's coordinates and re-renders the map. 4905 */ 4906 prepare() { 4907 if (Object.keys(this.tiles).length === 0 || this.video.readyState < 2) return; 4908 const context = this.map.painter.context; 4909 const gl = context.gl; 4910 if (!this.texture) { 4911 this.texture = new Texture(context, this.video, gl.RGBA); 4912 this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 4913 } else if (!this.video.paused) { 4914 this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 4915 gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.video); 4916 } 4917 let newTilesLoaded = false; 4918 for (const w in this.tiles) { 4919 const tile = this.tiles[w]; 4920 if (tile.state !== "loaded") { 4921 tile.state = "loaded"; 4922 tile.texture = this.texture; 4923 newTilesLoaded = true; 4924 } 4925 } 4926 if (newTilesLoaded) this.fire(new MapSourceDataEvent("data", { 4927 sourceDataType: "idle", 4928 sourceId: this.id 4929 })); 4930 } 4931 serialize() { 4932 return { 4933 type: "video", 4934 urls: this.urls, 4935 coordinates: this.coordinates 4936 }; 4937 } 4938 hasTransition() { 4939 return this.video && !this.video.paused; 4940 } 4941}; 4942//#endregion 4943//#region src/source/canvas_source.ts 4944/** 4945* A data source containing the contents of an HTML canvas. See {@link CanvasSourceSpecification} for detailed documentation of options. 4946* 4947* @group Sources 4948* 4949* @example 4950* ```ts 4951* // add to map 4952* map.addSource('some id', { 4953* type: 'canvas', 4954* canvas: 'idOfMyHTMLCanvas', 4955* animate: true, 4956* coordinates: [ 4957* [-76.54, 39.18], 4958* [-76.52, 39.18], 4959* [-76.52, 39.17], 4960* [-76.54, 39.17] 4961* ] 4962* }); 4963* 4964* // update 4965* let mySource = map.getSource('some id'); 4966* mySource.setCoordinates([ 4967* [-76.54335737228394, 39.18579907229748], 4968* [-76.52803659439087, 39.1838364847587], 4969* [-76.5295386314392, 39.17683392507606], 4970* [-76.54520273208618, 39.17876344106642] 4971* ]); 4972* 4973* map.removeSource('some id'); // remove 4974* ``` 4975*/ 4976var CanvasSource = class extends ImageSource { 4977 /** @internal */ 4978 constructor(id, options, dispatcher, eventedParent) { 4979 super(id, options, dispatcher, eventedParent); 4980 if (!options.coordinates) this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, "missing required property \"coordinates\""))); 4981 else if (!Array.isArray(options.coordinates) || options.coordinates.length !== 4 || options.coordinates.some((c) => !Array.isArray(c) || c.length !== 2 || c.some((l) => typeof l !== "number"))) this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, "\"coordinates\" property must be an array of 4 longitude/latitude array pairs"))); 4982 if (options.animate && typeof options.animate !== "boolean") this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, "optional \"animate\" property must be a boolean value"))); 4983 if (!options.canvas) this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, "missing required property \"canvas\""))); 4984 else if (typeof options.canvas !== "string" && !(options.canvas instanceof HTMLCanvasElement)) this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, "\"canvas\" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance"))); 4985 this.options = options; 4986 this.animate = options.animate !== void 0 ? options.animate : true; 4987 } 4988 async load() { 4989 this._loaded = true; 4990 this.canvas ||= this.options.canvas instanceof HTMLCanvasElement ? this.options.canvas : document.getElementById(this.options.canvas); 4991 this.width = this.canvas.width; 4992 this.height = this.canvas.height; 4993 if (this._hasInvalidDimensions()) { 4994 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Canvas dimensions cannot be less than or equal to zero."))); 4995 return; 4996 } 4997 this.play = function() { 4998 this._playing = true; 4999 this.map.triggerRepaint(); 5000 }; 5001 this.pause = function() { 5002 if (this._playing) { 5003 this.prepare(); 5004 this._playing = false; 5005 } 5006 }; 5007 this._finishLoading(); 5008 } 5009 /** 5010 * Returns the HTML `canvas` element. 5011 * 5012 * @returns The HTML `canvas` element. 5013 */ 5014 getCanvas() { 5015 return this.canvas; 5016 } 5017 onAdd(map) { 5018 this.map = map; 5019 this.load(); 5020 if (this.canvas) { 5021 if (this.animate) this.play(); 5022 } 5023 } 5024 onRemove() { 5025 this._playing = false; 5026 super.onRemove(); 5027 } 5028 prepare() { 5029 let resize = false; 5030 if (this.canvas.width !== this.width) { 5031 this.width = this.canvas.width; 5032 resize = true; 5033 } 5034 if (this.canvas.height !== this.height) { 5035 this.height = this.canvas.height; 5036 resize = true; 5037 } 5038 if (this._hasInvalidDimensions()) return; 5039 if (Object.keys(this.tiles).length === 0) return; 5040 const context = this.map.painter.context; 5041 const gl = context.gl; 5042 if (!this.texture) { 5043 this.texture = new Texture(context, this.canvas, gl.RGBA, { premultiply: true }); 5044 this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 5045 } else if (resize || this._playing) this.texture.update(this.canvas, { premultiply: true }); 5046 let newTilesLoaded = false; 5047 for (const w in this.tiles) { 5048 const tile = this.tiles[w]; 5049 if (tile.state !== "loaded") { 5050 tile.state = "loaded"; 5051 tile.texture = this.texture; 5052 newTilesLoaded = true; 5053 } 5054 } 5055 if (newTilesLoaded) this.fire(new MapSourceDataEvent("data", { 5056 sourceDataType: "idle", 5057 sourceId: this.id 5058 })); 5059 } 5060 serialize() { 5061 return { 5062 type: "canvas", 5063 animate: this.animate, 5064 canvas: this.options.canvas, 5065 coordinates: this.coordinates 5066 }; 5067 } 5068 hasTransition() { 5069 return this._playing; 5070 } 5071 _hasInvalidDimensions() { 5072 for (const x of [this.canvas.width, this.canvas.height]) if (isNaN(x) || x <= 0) return true; 5073 return false; 5074 } 5075}; 5076//#endregion 5077//#region src/source/source.ts 5078const registeredSources = {}; 5079/** 5080* Creates a tiled data source instance given an options object. 5081* 5082* @param id - The id for the source. Must not be used by any existing source. 5083* @param specification - Source options, specific to the source type (except for `options.type`, which is always required). 5084* @param source - A source definition object compliant with 5085* [`maplibre-gl-style-spec`](https://maplibre.org/maplibre-style-spec/#sources) or, for a third-party source type, 5086* with that type's requirements. 5087* @param dispatcher - A {@link Dispatcher} instance, which can be used to send messages to the workers. 5088* @returns a newly created source 5089*/ 5090const create = (id, specification, dispatcher, eventedParent) => { 5091 const source = new (getSourceType(specification.type))(id, specification, dispatcher, eventedParent); 5092 if (source.id !== id) throw new Error(`Expected Source id to be ${id} instead of ${source.id}`); 5093 return source; 5094}; 5095const getSourceType = (name) => { 5096 switch (name) { 5097 case "geojson": return GeoJSONSource; 5098 case "image": return ImageSource; 5099 case "raster": return RasterTileSource; 5100 case "raster-dem": return RasterDEMTileSource; 5101 case "vector": return VectorTileSource; 5102 case "video": return VideoSource; 5103 case "canvas": return CanvasSource; 5104 } 5105 return registeredSources[name]; 5106}; 5107const setSourceType = (name, type) => { 5108 registeredSources[name] = type; 5109}; 5110/** 5111* Adds a custom source type, making it available for use with {@link Map.addSource}. 5112* @param name - The name of the source type; source definition objects use this name in the `{type: ...}` field. 5113* @param SourceType - A {@link SourceClass} - which is a constructor for the `Source` interface. 5114* @returns a promise that is resolved when the source type is ready or rejected with an error. 5115*/ 5116const addSourceType = async (name, SourceType) => { 5117 if (getSourceType(name)) throw new Error(`A source type called "${name}" already exists.`); 5118 setSourceType(name, SourceType); 5119}; 5120//#endregion 5121//#region src/data/bucket.ts 5122function deserialize(input, style) { 5123 const output = {}; 5124 if (!style) return output; 5125 for (const bucket of input) { 5126 const layers = bucket.layerIds.map((id) => style.getLayer(id)).filter(Boolean); 5127 if (layers.length === 0) continue; 5128 bucket.layers = layers; 5129 if (bucket.stateDependentLayerIds) bucket.stateDependentLayers = bucket.stateDependentLayerIds.map((lId) => layers.filter((l) => l.id === lId)[0]); 5130 for (const layer of layers) output[layer.id] = bucket; 5131 } 5132 return output; 5133} 5134//#endregion 5135//#region src/source/rtl_text_plugin_status.ts 5136/** 5137* Fired once a plugin set through {@link setRTLTextPlugin} has loaded, so that tiles drawn before it 5138* arrived can be drawn again. 5139* 5140* @deprecated Right-to-left text is drawn without a plugin, so nothing has to be waited for. 5141*/ 5142const RTLPluginLoadedEventName = "RTLPluginLoaded"; 5143//#endregion 5144//#region src/source/rtl_text_plugin_main_thread.ts 5145var RTLMainThreadPlugin = class extends Evented { 5146 constructor(..._args) { 5147 super(..._args); 5148 this.status = "unavailable"; 5149 this.url = null; 5150 this.dispatcher = getGlobalDispatcher(); 5151 } 5152 /** Sync RTL plugin state by broadcasting a message to the worker */ 5153 _syncState(statusToSend) { 5154 this.status = statusToSend; 5155 return this.dispatcher.broadcast("SRPS", { 5156 pluginStatus: statusToSend, 5157 pluginURL: this.url 5158 }).catch((e) => { 5159 this.status = "error"; 5160 throw e; 5161 }); 5162 } 5163 /** This one is exposed to outside */ 5164 getRTLTextPluginStatus() { 5165 return this.status; 5166 } 5167 clearRTLTextPlugin() { 5168 this.status = "unavailable"; 5169 this.url = null; 5170 } 5171 async setRTLTextPlugin(url, deferred = false) { 5172 if (this.url) throw new Error("setRTLTextPlugin cannot be called multiple times."); 5173 this.url = browser.resolveURL(url); 5174 if (!this.url) throw new Error(`requested url ${url} is invalid`); 5175 if (this.status === "unavailable") { 5176 if (deferred) { 5177 this.status = "deferred"; 5178 this._syncState(this.status); 5179 } else return this._requestImport(); 5180 } else if (this.status === "requested") return this._requestImport(); 5181 } 5182 /** Send a message to worker which will import the RTL plugin script */ 5183 async _requestImport() { 5184 await this._syncState("loading"); 5185 this.status = "loaded"; 5186 this.fire(new Event(RTLPluginLoadedEventName)); 5187 } 5188 /** Start a lazy loading process of RTL plugin */ 5189 lazyLoad() { 5190 if (this.status === "unavailable") this.status = "requested"; 5191 else if (this.status === "deferred") this._requestImport(); 5192 } 5193}; 5194let rtlMainThreadPlugin = null; 5195function rtlMainThreadPluginFactory() { 5196 rtlMainThreadPlugin ||= new RTLMainThreadPlugin(); 5197 return rtlMainThreadPlugin; 5198} 5199//#endregion 5200//#region src/tile/tile.ts 5201const CLOCK_SKEW_RETRY_TIMEOUT = 3e4; 5202/** 5203* A tile object is the combination of a Coordinate, which defines 5204* its place, as well as a unique ID and data tracking for its content 5205*/ 5206var Tile = class { 5207 /** 5208 * @param tileID - the tile ID 5209 * @param size - The tile size 5210 */ 5211 constructor(tileID, size) { 5212 this.timeAdded = 0; 5213 this.fadeEndTime = 0; 5214 this.fadeOpacity = 1; 5215 this.tileID = tileID; 5216 this.uid = uniqueId(); 5217 this.uses = 0; 5218 this.tileSize = size; 5219 this.buckets = {}; 5220 this.expirationTime = null; 5221 this.queryPadding = 0; 5222 this.hasSymbolBuckets = false; 5223 this.hasRTLText = false; 5224 this.dependencies = {}; 5225 this.rttObjects = []; 5226 this.rttFingerprint = {}; 5227 this.expiredRequestCount = 0; 5228 this.state = "loading"; 5229 this.featureStateRevision = -1; 5230 } 5231 /** 5232 * Incoming and self-fading raster tiles must remain renderable at zero opacity because 5233 * drawing advances their opacity. Only transparent departing tiles have finished fading. 5234 */ 5235 isRenderable(symbolLayer) { 5236 return this.hasData() && (!this.fadeEndTime || this.fadingDirection !== 0 || this.fadeOpacity > 0) && (symbolLayer || !this.holdingForSymbolFade()); 5237 } 5238 /** 5239 * @internal 5240 * Many-to-one crossfade between a base tile and parent/ancestor tile (when zooming) 5241 */ 5242 setCrossFadeLogic({ fadingRole, fadingDirection, fadingParentID, fadeEndTime }) { 5243 this.resetFadeLogic(); 5244 this.fadingRole = fadingRole; 5245 this.fadingDirection = fadingDirection; 5246 this.fadingParentID = fadingParentID; 5247 this.fadeEndTime = fadeEndTime; 5248 } 5249 /** 5250 * Self fading for edge tiles (when panning map) 5251 */ 5252 setSelfFadeLogic(fadeEndTime) { 5253 this.resetFadeLogic(); 5254 this.selfFading = true; 5255 this.fadeEndTime = fadeEndTime; 5256 } 5257 resetFadeLogic() { 5258 this.fadingRole = null; 5259 this.fadingDirection = null; 5260 this.fadingParentID = null; 5261 this.selfFading = false; 5262 this.timeAdded = now(); 5263 this.fadeEndTime = 0; 5264 this.fadeOpacity = 1; 5265 } 5266 wasRequested() { 5267 return this.state === "errored" || this.state === "loaded" || this.state === "reloading"; 5268 } 5269 clearTextures(painter) { 5270 if (this.demTexture) painter.saveTileTexture(this.demTexture); 5271 this.demTexture = null; 5272 } 5273 /** 5274 * @internal 5275 * Returns the cached RTT object for this stack, or undefined on a cache miss. 5276 */ 5277 getRTT(stack) { 5278 return this.rttObjects[stack]; 5279 } 5280 /** 5281 * @internal 5282 * Allocates a fresh RTT object from the painter's pool and stores it at the 5283 * given stack slot. Callers should check {@link getRTT} first; calling this 5284 * over an existing slot leaks the previous object. 5285 */ 5286 acquireRTT(painter, stack, size) { 5287 return this.rttObjects[stack] = painter.acquireRTT(size); 5288 } 5289 /** 5290 * @internal 5291 * Returns all cached RTT slots to the painter's pool. 5292 */ 5293 releaseRTT(painter) { 5294 if (this.rttObjects.length === 0) return; 5295 for (const obj of this.rttObjects) if (obj) painter.releaseRTT(obj); 5296 this.rttObjects.length = 0; 5297 } 5298 /** 5299 * Given a data object with a 'buffers' property, load it into 5300 * this tile's elementGroups and buffers properties and set loaded 5301 * to true. If the data is null, like in the case of an empty 5302 * GeoJSON tile, no-op but still set loaded to true. 5303 * @param data - The data from the worker 5304 * @param painter - the painter 5305 * @param justReloaded - `true` to just reload 5306 */ 5307 loadVectorData(data, painter, justReloaded) { 5308 if (data?.etagUnmodified === true) { 5309 this.state = "loaded"; 5310 return; 5311 } 5312 if (this.hasData()) this.unloadVectorData(); 5313 this.state = "loaded"; 5314 if (!data) { 5315 this.collisionBoxArray = new CollisionBoxArray(); 5316 return; 5317 } 5318 if (data.featureIndex) { 5319 this.latestFeatureIndex = data.featureIndex; 5320 if (data.rawTileData) { 5321 this.latestRawTileData = data.rawTileData; 5322 this.latestEncoding = data.encoding; 5323 this.latestFeatureIndex.rawTileData = data.rawTileData; 5324 this.latestFeatureIndex.encoding = data.encoding; 5325 } else if (this.latestRawTileData) { 5326 this.latestFeatureIndex.rawTileData = this.latestRawTileData; 5327 this.latestFeatureIndex.encoding = this.latestEncoding; 5328 } 5329 } 5330 this.collisionBoxArray = data.collisionBoxArray; 5331 this.buckets = deserialize(data.buckets, painter?.style); 5332 this.hasSymbolBuckets = false; 5333 for (const id in this.buckets) { 5334 const bucket = this.buckets[id]; 5335 if (bucket instanceof SymbolBucket) { 5336 this.hasSymbolBuckets = true; 5337 if (justReloaded) bucket.justReloaded = true; 5338 else break; 5339 } 5340 } 5341 this.hasRTLText = false; 5342 if (this.hasSymbolBuckets) for (const id in this.buckets) { 5343 const bucket = this.buckets[id]; 5344 if (bucket instanceof SymbolBucket) { 5345 if (bucket.hasRTLText) { 5346 this.hasRTLText = true; 5347 rtlMainThreadPluginFactory().lazyLoad(); 5348 break; 5349 } 5350 } 5351 } 5352 this.queryPadding = 0; 5353 for (const id in this.buckets) { 5354 const bucket = this.buckets[id]; 5355 this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(id).queryRadius(bucket)); 5356 } 5357 if (data.imageAtlas) this.imageAtlas = data.imageAtlas; 5358 if (data.glyphAtlasImage) this.glyphAtlasImage = data.glyphAtlasImage; 5359 this.dashPositions = data.dashPositions; 5360 } 5361 /** 5362 * Release any data or WebGL resources referenced by this tile. 5363 */ 5364 unloadVectorData() { 5365 for (const id in this.buckets) this.buckets[id].destroy(); 5366 this.buckets = {}; 5367 if (this.imageAtlasTexture) this.imageAtlasTexture.destroy(); 5368 if (this.glyphAtlasTexture) this.glyphAtlasTexture.destroy(); 5369 this.imageAtlas = null; 5370 this.dashPositions = null; 5371 this.latestFeatureIndex = null; 5372 this.state = "unloaded"; 5373 } 5374 getBucket(layer) { 5375 return this.buckets[layer.id]; 5376 } 5377 upload(context) { 5378 for (const id in this.buckets) { 5379 const bucket = this.buckets[id]; 5380 if (bucket.uploadPending()) bucket.upload(context); 5381 } 5382 const gl = context.gl; 5383 if (this.imageAtlas && !this.imageAtlas.uploaded) { 5384 this.imageAtlasTexture = new Texture(context, this.imageAtlas.image, gl.RGBA); 5385 this.imageAtlas.uploaded = true; 5386 } 5387 if (this.glyphAtlasImage) { 5388 this.glyphAtlasTexture = new Texture(context, this.glyphAtlasImage, gl.ALPHA); 5389 this.glyphAtlasImage = null; 5390 } 5391 } 5392 prepare(imageManager) { 5393 if (this.imageAtlas) this.imageAtlas.patchUpdatedImages(imageManager, this.imageAtlasTexture); 5394 } 5395 queryRenderedFeatures(layers, serializedLayers, sourceFeatureState, queryGeometry, cameraQueryGeometry, scale, params, transform, maxPitchScaleFactor, pixelPosMatrix, getElevation) { 5396 if (!this.latestFeatureIndex?.rawTileData) return {}; 5397 return this.latestFeatureIndex.query({ 5398 queryGeometry, 5399 cameraQueryGeometry, 5400 scale, 5401 tileSize: this.tileSize, 5402 pixelPosMatrix, 5403 transform, 5404 params, 5405 queryPadding: this.queryPadding * maxPitchScaleFactor, 5406 getElevation 5407 }, layers, serializedLayers, sourceFeatureState); 5408 } 5409 querySourceFeatures(result, params) { 5410 const featureIndex = this.latestFeatureIndex; 5411 if (!featureIndex?.rawTileData) return; 5412 const vtLayers = featureIndex.loadVTLayers(); 5413 const sourceLayer = params?.sourceLayer ? params.sourceLayer : ""; 5414 const layer = vtLayers["_geojsonTileLayer"] || vtLayers[sourceLayer]; 5415 if (!layer) return; 5416 const filter = featureFilter(params?.filter, `querySourceFeatures[${sourceLayer}].filter`, params?.globalState); 5417 const { z, x, y } = this.tileID.canonical; 5418 const coord = { 5419 z, 5420 x, 5421 y 5422 }; 5423 for (let i = 0; i < layer.length; i++) { 5424 const feature = layer.feature(i); 5425 if (filter.needGeometry) { 5426 const evaluationFeature = toEvaluationFeature(feature, true); 5427 if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) continue; 5428 } else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) continue; 5429 const id = featureIndex.getId(feature, sourceLayer); 5430 const geojsonFeature = new GeoJSONFeature(feature, z, x, y, id); 5431 geojsonFeature.tile = coord; 5432 result.push(geojsonFeature); 5433 } 5434 } 5435 hasData() { 5436 return this.state === "loaded" || this.state === "reloading" || this.state === "expired"; 5437 } 5438 patternsLoaded() { 5439 return this.imageAtlas && !!Object.keys(this.imageAtlas.patternPositions).length; 5440 } 5441 setExpiryData(data) { 5442 const prior = this.expirationTime; 5443 if (data.cacheControl) { 5444 const parsedCC = parseCacheControl(data.cacheControl); 5445 if (parsedCC["max-age"]) this.expirationTime = Date.now() + parsedCC["max-age"] * 1e3; 5446 } else if (data.expires) this.expirationTime = new Date(data.expires).getTime(); 5447 if (this.expirationTime) { 5448 const now = Date.now(); 5449 let isExpired = false; 5450 if (this.expirationTime > now) isExpired = false; 5451 else if (!prior) isExpired = true; 5452 else if (this.expirationTime < prior) isExpired = true; 5453 else { 5454 const delta = this.expirationTime - prior; 5455 if (!delta) isExpired = true; 5456 else this.expirationTime = now + Math.max(delta, CLOCK_SKEW_RETRY_TIMEOUT); 5457 } 5458 if (isExpired) { 5459 this.expiredRequestCount++; 5460 this.state = "expired"; 5461 } else this.expiredRequestCount = 0; 5462 } 5463 } 5464 getExpiryTimeout() { 5465 if (this.expirationTime) { 5466 if (this.expiredRequestCount) return 1e3 * (1 << Math.min(this.expiredRequestCount - 1, 31)); 5467 else return Math.min(this.expirationTime - (/* @__PURE__ */ new Date()).getTime(), Math.pow(2, 31) - 1); 5468 } 5469 } 5470 setFeatureState(states, painter, revision) { 5471 if (!this.latestFeatureIndex?.rawTileData || Object.keys(states).length === 0) return; 5472 if (this.featureStateRevision === revision) return; 5473 this.featureStateRevision = revision; 5474 const vtLayers = this.latestFeatureIndex.loadVTLayers(); 5475 for (const id in this.buckets) { 5476 if (!painter.style.hasLayer(id)) continue; 5477 const bucket = this.buckets[id]; 5478 const sourceLayerId = bucket.layers[0]["sourceLayer"] || "_geojsonTileLayer"; 5479 const sourceLayer = vtLayers[sourceLayerId]; 5480 const sourceLayerStates = states[sourceLayerId]; 5481 if (!sourceLayer || !sourceLayerStates || sourceLayerStates.length === 0) continue; 5482 bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas?.patternPositions || {}, this.dashPositions || {}); 5483 const layer = painter?.style?.getLayer(id); 5484 if (layer) this.queryPadding = Math.max(this.queryPadding, layer.queryRadius(bucket)); 5485 } 5486 } 5487 holdingForSymbolFade() { 5488 return this.symbolFadeHoldUntil !== void 0; 5489 } 5490 symbolFadeFinished() { 5491 return !this.symbolFadeHoldUntil || this.symbolFadeHoldUntil < now(); 5492 } 5493 clearSymbolFadeHold() { 5494 this.symbolFadeHoldUntil = void 0; 5495 } 5496 setSymbolHoldDuration(duration) { 5497 this.symbolFadeHoldUntil = now() + duration; 5498 } 5499 setDependencies(namespace, dependencies) { 5500 const index = {}; 5501 for (const dep of dependencies) index[dep] = true; 5502 this.dependencies[namespace] = index; 5503 } 5504 hasDependency(namespaces, keys) { 5505 for (const namespace of namespaces) { 5506 const dependencies = this.dependencies[namespace]; 5507 if (dependencies) { 5508 for (const key of keys) if (dependencies[key]) return true; 5509 } 5510 } 5511 return false; 5512 } 5513}; 5514//#endregion 5515//#region src/source/source_state.ts 5516function featureStatesMapToArray(map) { 5517 const result = []; 5518 for (const id in map) result.push({ 5519 id, 5520 state: map[id] 5521 }); 5522 return result; 5523} 5524/** 5525* @internal 5526* SourceFeatureState manages the state and pending changes 5527* to features in a source, separated by source layer. 5528* stateChanges and deletedStates batch all changes to the tile (updates and removes, respectively) 5529* between coalesce() events. addFeatureState() and removeFeatureState() also update their counterpart's 5530* list of changes, such that coalesce() can apply the proper state changes while agnostic to the order of operations. 5531* In deletedStates, all null's denote complete removal of state at that scope 5532*/ 5533var SourceFeatureState = class { 5534 constructor() { 5535 this.state = {}; 5536 this.stateChanges = {}; 5537 this.deletedStates = {}; 5538 this.revision = 0; 5539 } 5540 updateState(sourceLayer, featureId, newState) { 5541 const feature = String(featureId); 5542 this.stateChanges[sourceLayer] ||= {}; 5543 this.stateChanges[sourceLayer][feature] ||= {}; 5544 extend(this.stateChanges[sourceLayer][feature], newState); 5545 if (this.deletedStates[sourceLayer] === null) { 5546 this.deletedStates[sourceLayer] = {}; 5547 for (const ft in this.state[sourceLayer]) if (ft === feature) { 5548 this.deletedStates[sourceLayer][feature] = {}; 5549 for (const prop in this.state[sourceLayer][ft]) if (newState[prop] === void 0) this.deletedStates[sourceLayer][feature][prop] = null; 5550 } else this.deletedStates[sourceLayer][ft] = null; 5551 } else if (this.deletedStates[sourceLayer]?.[feature] === null) { 5552 this.deletedStates[sourceLayer][feature] = {}; 5553 for (const prop in this.state[sourceLayer][feature]) if (newState[prop] === void 0) this.deletedStates[sourceLayer][feature][prop] = null; 5554 } else for (const key in newState) if (this.deletedStates[sourceLayer]?.[feature]?.[key] === null) delete this.deletedStates[sourceLayer][feature][key]; 5555 } 5556 removeFeatureState(sourceLayer, featureId, key) { 5557 if (this.deletedStates[sourceLayer] === null) return; 5558 const feature = String(featureId); 5559 this.deletedStates[sourceLayer] ||= {}; 5560 if (key && featureId !== void 0) { 5561 if (this.deletedStates[sourceLayer][feature] !== null) { 5562 this.deletedStates[sourceLayer][feature] ||= {}; 5563 this.deletedStates[sourceLayer][feature][key] = null; 5564 } 5565 } else if (featureId !== void 0) { 5566 if (this.stateChanges[sourceLayer]?.[feature]) { 5567 this.deletedStates[sourceLayer][feature] = {}; 5568 for (key in this.stateChanges[sourceLayer][feature]) this.deletedStates[sourceLayer][feature][key] = null; 5569 } else this.deletedStates[sourceLayer][feature] = null; 5570 } else this.deletedStates[sourceLayer] = null; 5571 } 5572 getState(sourceLayer, featureId) { 5573 const feature = String(featureId); 5574 const base = this.state[sourceLayer] || {}; 5575 const changes = this.stateChanges[sourceLayer] || {}; 5576 const reconciledState = extend({}, base[feature], changes[feature]); 5577 if (this.deletedStates[sourceLayer] === null) return {}; 5578 else if (this.deletedStates[sourceLayer]) { 5579 const featureDeletions = this.deletedStates[sourceLayer][featureId]; 5580 if (featureDeletions === null) return {}; 5581 for (const prop in featureDeletions) delete reconciledState[prop]; 5582 } 5583 return reconciledState; 5584 } 5585 initializeTileState(tile, painter) { 5586 const layerStates = {}; 5587 for (const sourceLayer in this.state) layerStates[sourceLayer] = featureStatesMapToArray(this.state[sourceLayer]); 5588 tile.setFeatureState(layerStates, painter, this.revision); 5589 } 5590 coalesceChanges(inViewTiles, painter) { 5591 const featuresChangedMap = {}; 5592 for (const sourceLayer in this.stateChanges) { 5593 this.state[sourceLayer] ||= {}; 5594 featuresChangedMap[sourceLayer] ||= {}; 5595 for (const feature in this.stateChanges[sourceLayer]) { 5596 this.state[sourceLayer][feature] ||= {}; 5597 extend(this.state[sourceLayer][feature], this.stateChanges[sourceLayer][feature]); 5598 featuresChangedMap[sourceLayer][feature] = this.state[sourceLayer][feature]; 5599 } 5600 } 5601 for (const sourceLayer in this.deletedStates) { 5602 this.state[sourceLayer] ||= {}; 5603 featuresChangedMap[sourceLayer] ||= {}; 5604 if (this.deletedStates[sourceLayer] === null) for (const ft in this.state[sourceLayer]) { 5605 this.state[sourceLayer][ft] = {}; 5606 featuresChangedMap[sourceLayer][ft] = {}; 5607 } 5608 else for (const feature in this.deletedStates[sourceLayer]) { 5609 if (this.deletedStates[sourceLayer][feature] === null) this.state[sourceLayer][feature] = {}; 5610 else for (const key of Object.keys(this.deletedStates[sourceLayer][feature])) delete this.state[sourceLayer][feature][key]; 5611 featuresChangedMap[sourceLayer][feature] = this.state[sourceLayer][feature]; 5612 } 5613 } 5614 this.stateChanges = {}; 5615 this.deletedStates = {}; 5616 if (Object.keys(featuresChangedMap).length === 0) return; 5617 this.revision++; 5618 const featuresChanged = {}; 5619 for (const sourceLayer in featuresChangedMap) featuresChanged[sourceLayer] = featureStatesMapToArray(featuresChangedMap[sourceLayer]); 5620 inViewTiles.setFeatureState(featuresChanged, painter, this.revision); 5621 } 5622}; 5623//#endregion 5624//#region src/util/primitives/aabb.ts 5625var Aabb = class Aabb { 5626 constructor(min_, max_) { 5627 this.min = min_; 5628 this.max = max_; 5629 this.center = scale$1([], add([], this.min, this.max), .5); 5630 } 5631 quadrant(index) { 5632 const split = [index % 2 === 0, index < 2]; 5633 const qMin = clone(this.min); 5634 const qMax = clone(this.max); 5635 for (let axis = 0; axis < split.length; axis++) { 5636 qMin[axis] = split[axis] ? this.min[axis] : this.center[axis]; 5637 qMax[axis] = split[axis] ? this.center[axis] : this.max[axis]; 5638 } 5639 qMax[2] = this.max[2]; 5640 return new Aabb(qMin, qMax); 5641 } 5642 distanceX(point) { 5643 return Math.max(Math.min(this.max[0], point[0]), this.min[0]) - point[0]; 5644 } 5645 distanceY(point) { 5646 return Math.max(Math.min(this.max[1], point[1]), this.min[1]) - point[1]; 5647 } 5648 /** 5649 * Performs a frustum-aabb intersection test. 5650 */ 5651 intersectsFrustum(frustum) { 5652 let fullyInside = true; 5653 for (const plane of frustum.planes) { 5654 const planeIntersection = this.intersectsPlane(plane); 5655 if (planeIntersection === 0) return 0; 5656 if (planeIntersection === 1) fullyInside = false; 5657 } 5658 if (fullyInside) return 2; 5659 if (frustum.aabb.min[0] > this.max[0] || frustum.aabb.min[1] > this.max[1] || frustum.aabb.min[2] > this.max[2] || frustum.aabb.max[0] < this.min[0] || frustum.aabb.max[1] < this.min[1] || frustum.aabb.max[2] < this.min[2]) return 0; 5660 return 1; 5661 } 5662 /** 5663 * Performs a halfspace-aabb intersection test. 5664 */ 5665 intersectsPlane(plane) { 5666 let distMin = plane[3]; 5667 let distMax = plane[3]; 5668 for (let i = 0; i < 3; i++) if (plane[i] > 0) { 5669 distMin += plane[i] * this.min[i]; 5670 distMax += plane[i] * this.max[i]; 5671 } else { 5672 distMax += plane[i] * this.min[i]; 5673 distMin += plane[i] * this.max[i]; 5674 } 5675 if (distMin >= 0) return 2; 5676 if (distMax < 0) return 0; 5677 return 1; 5678 } 5679}; 5680//#endregion 5681//#region src/util/primitives/frustum.ts 5682var Frustum = class Frustum { 5683 constructor(points, planes, aabb) { 5684 this.points = points; 5685 this.planes = planes; 5686 this.aabb = aabb; 5687 } 5688 static fromInvProjectionMatrix(invProj, worldSize = 1, zoom = 0, horizonPlane, flippedNearFar) { 5689 const clipSpaceCorners = [ 5690 [ 5691 -1, 5692 1, 5693 -1, 5694 1 5695 ], 5696 [ 5697 1, 5698 1, 5699 -1, 5700 1 5701 ], 5702 [ 5703 1, 5704 -1, 5705 -1, 5706 1 5707 ], 5708 [ 5709 -1, 5710 -1, 5711 -1, 5712 1 5713 ], 5714 [ 5715 -1, 5716 1, 5717 1, 5718 1 5719 ], 5720 [ 5721 1, 5722 1, 5723 1, 5724 1 5725 ], 5726 [ 5727 1, 5728 -1, 5729 1, 5730 1 5731 ], 5732 [ 5733 -1, 5734 -1, 5735 1, 5736 1 5737 ] 5738 ]; 5739 const frustumPlanePointIndices = flippedNearFar ? [ 5740 [ 5741 6, 5742 5, 5743 4 5744 ], 5745 [ 5746 0, 5747 1, 5748 2 5749 ], 5750 [ 5751 0, 5752 3, 5753 7 5754 ], 5755 [ 5756 2, 5757 1, 5758 5 5759 ], 5760 [ 5761 3, 5762 2, 5763 6 5764 ], 5765 [ 5766 0, 5767 4, 5768 5 5769 ] 5770 ] : [ 5771 [ 5772 0, 5773 1, 5774 2 5775 ], 5776 [ 5777 6, 5778 5, 5779 4 5780 ], 5781 [ 5782 0, 5783 3, 5784 7 5785 ], 5786 [ 5787 2, 5788 1, 5789 5 5790 ], 5791 [ 5792 3, 5793 2, 5794 6 5795 ], 5796 [ 5797 0, 5798 4, 5799 5 5800 ] 5801 ]; 5802 const scale = Math.pow(2, zoom); 5803 const frustumCoords = clipSpaceCorners.map((v) => unprojectClipSpacePoint(v, invProj, worldSize, scale)); 5804 if (horizonPlane) adjustFarPlaneByHorizonPlane(frustumCoords, frustumPlanePointIndices[0], horizonPlane, flippedNearFar); 5805 const frustumPlanes = frustumPlanePointIndices.map((p) => { 5806 const a = sub([], frustumCoords[p[0]], frustumCoords[p[1]]); 5807 const b = sub([], frustumCoords[p[2]], frustumCoords[p[1]]); 5808 const n = normalize([], cross([], a, b)); 5809 const d = -dot(n, frustumCoords[p[1]]); 5810 return n.concat(d); 5811 }); 5812 const min = [ 5813 Number.POSITIVE_INFINITY, 5814 Number.POSITIVE_INFINITY, 5815 Number.POSITIVE_INFINITY 5816 ]; 5817 const max = [ 5818 Number.NEGATIVE_INFINITY, 5819 Number.NEGATIVE_INFINITY, 5820 Number.NEGATIVE_INFINITY 5821 ]; 5822 for (const p of frustumCoords) for (let i = 0; i < 3; i++) { 5823 min[i] = Math.min(min[i], p[i]); 5824 max[i] = Math.max(max[i], p[i]); 5825 } 5826 return new Frustum(frustumCoords, frustumPlanes, new Aabb(min, max)); 5827 } 5828}; 5829function unprojectClipSpacePoint(point, invProj, worldSize, scale) { 5830 const v = transformMat4([], point, invProj); 5831 const s = 1 / v[3] / worldSize * scale; 5832 return mul(v, v, [ 5833 s, 5834 s, 5835 1 / v[3], 5836 s 5837 ]); 5838} 5839/** 5840* Modifies points in the supplied `frustumCoords` array so that the frustum's far plane only lies as far as the horizon, 5841* which improves frustum culling effectiveness. 5842* @param frustumCoords - Points of the frustum. 5843* @param nearPlanePointsIndices - Which indices in the `frustumCoords` form the near plane. 5844* @param horizonPlane - The horizon plane. 5845*/ 5846function adjustFarPlaneByHorizonPlane(frustumCoords, nearPlanePointsIndices, horizonPlane, flippedNearFar) { 5847 const nearPlanePointsOffset = flippedNearFar ? 4 : 0; 5848 const farPlanePointsOffset = flippedNearFar ? 0 : 4; 5849 let maxDist = 0; 5850 const cornerRayLengths = []; 5851 const cornerRayNormalizedDirections = []; 5852 for (let i = 0; i < 4; i++) { 5853 const dir = sub([], frustumCoords[i + farPlanePointsOffset], frustumCoords[i + nearPlanePointsOffset]); 5854 const len = length(dir); 5855 scale$1(dir, dir, 1 / len); 5856 cornerRayLengths.push(len); 5857 cornerRayNormalizedDirections.push(dir); 5858 } 5859 for (let i = 0; i < 4; i++) { 5860 const dist = rayPlaneIntersection(frustumCoords[i + nearPlanePointsOffset], cornerRayNormalizedDirections[i], horizonPlane); 5861 if (dist !== null && dist >= 0) maxDist = Math.max(maxDist, dist); 5862 else maxDist = Math.max(maxDist, cornerRayLengths[i]); 5863 } 5864 const nearPlaneNormalized = getNormalizedNearPlane(frustumCoords, nearPlanePointsIndices); 5865 const idealFarPlaneDistanceFromNearPlane = getIdealNearFarPlaneDistance(horizonPlane, nearPlaneNormalized); 5866 if (idealFarPlaneDistanceFromNearPlane !== null) { 5867 const idealCornerRayLength = idealFarPlaneDistanceFromNearPlane / dot(cornerRayNormalizedDirections[0], nearPlaneNormalized); 5868 maxDist = Math.min(maxDist, idealCornerRayLength); 5869 } 5870 for (let i = 0; i < 4; i++) { 5871 const targetLength = Math.min(maxDist, cornerRayLengths[i]); 5872 frustumCoords[i + farPlanePointsOffset] = [ 5873 frustumCoords[i + nearPlanePointsOffset][0] + cornerRayNormalizedDirections[i][0] * targetLength, 5874 frustumCoords[i + nearPlanePointsOffset][1] + cornerRayNormalizedDirections[i][1] * targetLength, 5875 frustumCoords[i + nearPlanePointsOffset][2] + cornerRayNormalizedDirections[i][2] * targetLength, 5876 1 5877 ]; 5878 } 5879} 5880/** 5881* Returns the near plane equation with unit length direction. 5882* @param frustumCoords - Points of the frustum. 5883* @param nearPlanePointsIndices - Which indices in the `frustumCoords` form the near plane. 5884*/ 5885function getNormalizedNearPlane(frustumCoords, nearPlanePointsIndices) { 5886 const nearPlaneA = sub([], frustumCoords[nearPlanePointsIndices[0]], frustumCoords[nearPlanePointsIndices[1]]); 5887 const nearPlaneB = sub([], frustumCoords[nearPlanePointsIndices[2]], frustumCoords[nearPlanePointsIndices[1]]); 5888 const nearPlaneNormalized = [ 5889 0, 5890 0, 5891 0, 5892 0 5893 ]; 5894 normalize(nearPlaneNormalized, cross([], nearPlaneA, nearPlaneB)); 5895 nearPlaneNormalized[3] = -dot(nearPlaneNormalized, frustumCoords[nearPlanePointsIndices[0]]); 5896 return nearPlaneNormalized; 5897} 5898/** 5899* Returns the ideal distance between the frustum's near and far plane so that the far plane only lies as far as the horizon. 5900*/ 5901function getIdealNearFarPlaneDistance(horizonPlane, nearPlaneNormalized) { 5902 const horizonPlaneLen = len(horizonPlane); 5903 const normalizedHorizonPlane = scale$2([], horizonPlane, 1 / horizonPlaneLen); 5904 const projectedViewDirection = sub([], nearPlaneNormalized, scale$1([], normalizedHorizonPlane, dot(nearPlaneNormalized, normalizedHorizonPlane))); 5905 const projectedViewLength = len(projectedViewDirection); 5906 if (projectedViewLength > 0) { 5907 const horizonCircleRadius = Math.sqrt(1 - normalizedHorizonPlane[3] * normalizedHorizonPlane[3]); 5908 const horizonCircleCenter = scale$1([], normalizedHorizonPlane, -normalizedHorizonPlane[3]); 5909 const pointFurthestOnHorizonCircle = add([], horizonCircleCenter, scale$1([], projectedViewDirection, horizonCircleRadius / projectedViewLength)); 5910 return pointPlaneSignedDistance(nearPlaneNormalized, pointFurthestOnHorizonCircle); 5911 } else return null; 5912} 5913//#endregion 5914//#region src/geo/projection/covering_tiles.ts 5915/** 5916* A simple/heuristic function that returns whether the tile is visible under the current transform. 5917* @returns an {@link IntersectionResult}. 5918*/ 5919function isTileVisible(frustum, tileBoundingVolume, plane) { 5920 const frustumTest = tileBoundingVolume.intersectsFrustum(frustum); 5921 if (!plane || frustumTest === 0) return frustumTest; 5922 const planeTest = tileBoundingVolume.intersectsPlane(plane); 5923 if (planeTest === 0) return 0; 5924 if (frustumTest === 2 && planeTest === 2) return 2; 5925 return 1; 5926} 5927/** 5928* Definite integral of cos(x)^p. The analytical solution is described in `developer-guides/covering-tiles.md`, 5929* but here the integral is evaluated numerically. 5930* @param p - the power to raise cos(x) to inside the integral 5931* @param x1 - the starting point of the integral. 5932* @param x2 - the ending point of the integral. 5933* @return the integral of cos(x)^p from x=x1 to x=x2 5934*/ 5935function integralOfCosXByP(p, x1, x2) { 5936 const numPoints = 10; 5937 let sum = 0; 5938 const dx = (x2 - x1) / numPoints; 5939 for (let i = 0; i < numPoints; i++) { 5940 const x = x1 + (i + .5) / numPoints * (x2 - x1); 5941 sum += dx * Math.pow(Math.cos(x), p); 5942 } 5943 return sum; 5944} 5945function createCalculateTileZoomFunction(maxZoomLevelsOnScreen, tileCountMaxMinRatio) { 5946 return function(requestedCenterZoom, distanceToTile2D, distanceToTileZ, distanceToCenter3D, cameraVerticalFOV) { 5947 /** 5948 * Controls how tiles are loaded at high pitch angles. Higher numbers cause fewer, lower resolution 5949 * tiles to be loaded. Calculate the value that will result in the selected number of zoom levels in 5950 * the worst-case condition (when the horizon is at the top of the screen). For more information, see 5951 * `developer-guides/covering-tiles.md` 5952 */ 5953 const pitchTileLoadingBehavior = 2 * ((maxZoomLevelsOnScreen - 1) / scaleZoom(Math.cos(degreesToRadians(maxMercatorHorizonAngle - cameraVerticalFOV)) / Math.cos(degreesToRadians(maxMercatorHorizonAngle))) - 1); 5954 const centerPitch = Math.acos(distanceToTileZ / distanceToCenter3D); 5955 const tileCountPitch0 = 2 * integralOfCosXByP(pitchTileLoadingBehavior - 1, 0, degreesToRadians(cameraVerticalFOV / 2)); 5956 const highestPitch = Math.min(degreesToRadians(maxMercatorHorizonAngle), centerPitch + degreesToRadians(cameraVerticalFOV / 2)); 5957 const lowestPitch = Math.min(highestPitch, centerPitch - degreesToRadians(cameraVerticalFOV / 2)); 5958 const tileCount = integralOfCosXByP(pitchTileLoadingBehavior - 1, lowestPitch, highestPitch); 5959 const thisTilePitch = Math.atan(distanceToTile2D / distanceToTileZ); 5960 const distanceToTile3D = Math.hypot(distanceToTile2D, distanceToTileZ); 5961 let thisTileDesiredZ = requestedCenterZoom; 5962 thisTileDesiredZ = thisTileDesiredZ + scaleZoom(distanceToCenter3D / distanceToTile3D / Math.max(.5, Math.cos(degreesToRadians(cameraVerticalFOV / 2)))); 5963 thisTileDesiredZ += pitchTileLoadingBehavior * scaleZoom(Math.cos(thisTilePitch)) / 2; 5964 thisTileDesiredZ -= scaleZoom(Math.max(1, tileCount / tileCountPitch0 / tileCountMaxMinRatio)) / 2; 5965 return thisTileDesiredZ; 5966 }; 5967} 5968const defaultCalculateTileZoom = createCalculateTileZoomFunction(9.314, 3); 5969/** 5970* Return what zoom level of a tile source would most closely cover the tiles displayed by this transform. 5971* @param options - The options, most importantly the source's tile size. 5972* @returns An integer zoom level at which all tiles will be visible. 5973*/ 5974function coveringZoomLevel(transform, options) { 5975 const z = (options.roundZoom ? Math.round : Math.floor)(transform.zoom + scaleZoom(transform.tileSize / options.tileSize)); 5976 return Math.max(0, z); 5977} 5978/** 5979* Without terrain, `getTileBoundingVolume` has no knowledge of extruded features 5980* (eg. 3D buildings): every tile's bounding box is flat at the camera's `elevation`. 5981* That's a fine approximation of what's visible for most views, but it breaks down 5982* as the frustum's bottom edge approaches horizontal (which depends on both `pitch` 5983* and `fov`, not pitch alone) - a tall building can still be poking up into view 5984* long after its tile's ground point has dropped out of the frustum. 5985* 5986* This is an assumed upper bound on real-world feature height (in metres), used to 5987* grow the elevation used for tile culling as that bottom edge nears horizontal, so 5988* such tiles are not dropped too early. It has no effect away from that edge case. 5989*/ 5990const ASSUMED_MAX_FEATURE_HEIGHT_METERS = 500; 5991/** 5992* Angle between the frustum's bottom edge and the mercator horizon below which 5993* the culling elevation starts to grow. 5994*/ 5995const TILE_CULLING_HORIZON_ONSET_DEGREES = 15; 5996/** 5997* Returns the elevation to use when computing tile bounding volumes for culling: 5998* `transform.elevation`, growing by up to the greater of 5999* `ASSUMED_MAX_FEATURE_HEIGHT_METERS` and `maxContentElevation` (when provided) 6000* as the frustum's bottom edge approaches the horizon, where a ground-level 6001* bounding box would cull tiles whose extruded features are still visible. 6002*/ 6003function getElevationForTileCulling(transform, maxContentElevation) { 6004 const bottomEdgeDegreesAboveHorizontal = maxMercatorHorizonAngle - transform.pitch - transform.fov / 2; 6005 const proximityToHorizon = clamp((TILE_CULLING_HORIZON_ONSET_DEGREES - bottomEdgeDegreesAboveHorizontal) / TILE_CULLING_HORIZON_ONSET_DEGREES, 0, 1); 6006 const maxFeatureHeight = Math.max(ASSUMED_MAX_FEATURE_HEIGHT_METERS, maxContentElevation ?? 0); 6007 return transform.elevation + proximityToHorizon * maxFeatureHeight; 6008} 6009/** 6010* Returns a copy of the frustum with its far plane moved away from the camera by `distance`. 6011* Each far corner travels along its own camera ray until it reaches the pushed plane, so the 6012* result stays a pyramid and the corners stay consistent with the side planes. The globe 6013* frustum's far plane is fitted to the surface horizon, so elevated content beyond it would 6014* otherwise be culled while still visible. 6015*/ 6016function pushFrustumFarPlane(frustum, distance, cameraPos) { 6017 const far = frustum.planes[1]; 6018 const points = frustum.points.map((p) => { 6019 const distanceToFar = far[0] * p[0] + far[1] * p[1] + far[2] * p[2] + far[3]; 6020 if (Math.abs(distanceToFar) > 1e-6) return p; 6021 const ray = [ 6022 p[0] - cameraPos[0], 6023 p[1] - cameraPos[1], 6024 p[2] - cameraPos[2] 6025 ]; 6026 const rayDotNormal = far[0] * ray[0] + far[1] * ray[1] + far[2] * ray[2]; 6027 if (rayDotNormal >= -1e-9) return p; 6028 const scale = distance / -rayDotNormal; 6029 return [ 6030 p[0] + ray[0] * scale, 6031 p[1] + ray[1] * scale, 6032 p[2] + ray[2] * scale, 6033 p[3] 6034 ]; 6035 }); 6036 const planes = frustum.planes.map((p, i) => i === 1 ? [ 6037 p[0], 6038 p[1], 6039 p[2], 6040 p[3] + distance 6041 ] : p); 6042 const min = [ 6043 Number.POSITIVE_INFINITY, 6044 Number.POSITIVE_INFINITY, 6045 Number.POSITIVE_INFINITY 6046 ]; 6047 const max = [ 6048 Number.NEGATIVE_INFINITY, 6049 Number.NEGATIVE_INFINITY, 6050 Number.NEGATIVE_INFINITY 6051 ]; 6052 for (const p of points) for (let i = 0; i < 3; i++) { 6053 min[i] = Math.min(min[i], p[i]); 6054 max[i] = Math.max(max[i], p[i]); 6055 } 6056 return new Frustum(points, planes, new Aabb(min, max)); 6057} 6058/** 6059* The horizon culling plane and the frustum's far plane both assume content sits on the 6060* surface. Content elevated to radius r (in planet radii) stays visible up to acos(1/r) 6061* beyond the surface horizon and up to sqrt(r^2-1) farther away, so both are pushed back 6062* accordingly; without this, tiles under highly elevated symbols would be dropped while 6063* the symbols are still in view. 6064* @param frustum - The covering frustum, fitted to the surface horizon. 6065* @param plane - The horizon culling plane used by globe transform. 6066* @param cameraPos - The camera position, in the frustum's space. 6067* @param maxContentElevation - The highest elevation the content may reach, in meters. 6068*/ 6069function expandCullingToContentElevation(frustum, plane, cameraPos, maxContentElevation) { 6070 const len = Math.hypot(plane[0], plane[1], plane[2]); 6071 if (len === 0) return { 6072 frustum, 6073 plane 6074 }; 6075 const shellRadius = 1 + maxContentElevation / earthRadius; 6076 const horizonCos = clamp(-plane[3] / len, -1, 1); 6077 const shellCos = Math.cos(Math.acos(horizonCos) + Math.acos(1 / shellRadius)); 6078 return { 6079 frustum: pushFrustumFarPlane(frustum, Math.sqrt(shellRadius * shellRadius - 1), cameraPos), 6080 plane: [ 6081 plane[0], 6082 plane[1], 6083 plane[2], 6084 -shellCos * len 6085 ] 6086 }; 6087} 6088/** 6089* Returns a list of tiles that optimally covers the screen. Adapted for globe projection. 6090* Correctly handles LOD when moving over the antimeridian. 6091* @param transform - The transform instance. 6092* @param frustum - The covering frustum. 6093* @param plane - The clipping plane used by globe transform, or null. 6094* @param cameraCoord - The x, y, z position of the camera in MercatorCoordinates. 6095* @param centerCoord - The x, y, z position of the center point in MercatorCoordinates. 6096* @param options - Additional coveringTiles options. 6097* @param details - Interface to define required helper functions. 6098* @returns A list of tile coordinates, ordered by ascending distance from camera. 6099*/ 6100function coveringTiles(transform, options) { 6101 let frustum = transform.getCameraFrustum(); 6102 let plane = transform.getClippingPlane(); 6103 if (plane && options.maxContentElevation > 0) ({frustum, plane} = expandCullingToContentElevation(frustum, plane, transform.cameraPosition, options.maxContentElevation)); 6104 const cameraCoord = cameraMercatorCoordinate(transform); 6105 const centerCoord = MercatorCoordinate.fromLngLat(transform.center, transform.elevation); 6106 const elevationForTileCulling = getElevationForTileCulling(transform, options.maxContentElevation); 6107 const detailsProvider = transform.getCoveringTilesDetailsProvider(); 6108 const allowVariableZoom = detailsProvider.allowVariableZoom(transform, options); 6109 const desiredZ = coveringZoomLevel(transform, options); 6110 const minZoom = options.minzoom || 0; 6111 const maxZoom = options.maxzoom !== void 0 ? options.maxzoom : transform.maxZoom; 6112 const nominalZ = Math.min(Math.max(0, desiredZ), maxZoom); 6113 const numTiles = Math.pow(2, nominalZ); 6114 const cameraPoint = [ 6115 numTiles * cameraCoord.x, 6116 numTiles * cameraCoord.y, 6117 0 6118 ]; 6119 const centerPoint = [ 6120 numTiles * centerCoord.x, 6121 numTiles * centerCoord.y, 6122 0 6123 ]; 6124 const distanceToCenter2d = Math.hypot(centerCoord.x - cameraCoord.x, centerCoord.y - cameraCoord.y); 6125 const distanceZ = Math.abs(centerCoord.z - cameraCoord.z); 6126 const distanceToCenter3d = Math.hypot(distanceToCenter2d, distanceZ); 6127 const newRootTile = (wrap) => { 6128 return { 6129 zoom: 0, 6130 x: 0, 6131 y: 0, 6132 wrap, 6133 fullyVisible: false 6134 }; 6135 }; 6136 const stack = []; 6137 const result = []; 6138 if (transform.renderWorldCopies && detailsProvider.allowWorldCopies()) for (let i = 1; i <= 3; i++) { 6139 stack.push(newRootTile(-i)); 6140 stack.push(newRootTile(i)); 6141 } 6142 stack.push(newRootTile(0)); 6143 while (stack.length > 0) { 6144 const it = stack.pop(); 6145 const x = it.x; 6146 const y = it.y; 6147 let fullyVisible = it.fullyVisible; 6148 const tileID = { 6149 x, 6150 y, 6151 z: it.zoom 6152 }; 6153 const boundingVolume = detailsProvider.getTileBoundingVolume(tileID, it.wrap, elevationForTileCulling, options); 6154 if (!fullyVisible) { 6155 const intersectResult = isTileVisible(frustum, boundingVolume, plane); 6156 if (intersectResult === 0) continue; 6157 fullyVisible = intersectResult === 2; 6158 } 6159 const distToTile2d = detailsProvider.distanceToTile2d(cameraCoord.x, cameraCoord.y, tileID, boundingVolume); 6160 let thisTileDesiredZ = desiredZ; 6161 if (allowVariableZoom) thisTileDesiredZ = (options.calculateTileZoom || defaultCalculateTileZoom)(transform.zoom + scaleZoom(transform.tileSize / options.tileSize), distToTile2d, distanceZ, distanceToCenter3d, transform.fov); 6162 thisTileDesiredZ = (options.roundZoom ? Math.round : Math.floor)(thisTileDesiredZ); 6163 thisTileDesiredZ = Math.max(0, thisTileDesiredZ); 6164 const z = Math.min(thisTileDesiredZ, maxZoom); 6165 it.wrap = detailsProvider.getWrap(centerCoord, tileID, it.wrap); 6166 if (it.zoom >= z) { 6167 if (it.zoom < minZoom) continue; 6168 const dz = nominalZ - it.zoom; 6169 const dx = cameraPoint[0] - .5 - (x << dz); 6170 const dy = cameraPoint[1] - .5 - (y << dz); 6171 const overscaledZ = options.reparseOverscaled ? Math.max(it.zoom, thisTileDesiredZ) : it.zoom; 6172 result.push({ 6173 tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y), 6174 distanceSq: sqrLen([centerPoint[0] - .5 - x, centerPoint[1] - .5 - y]), 6175 tileDistanceToCamera: Math.sqrt(dx * dx + dy * dy) 6176 }); 6177 continue; 6178 } 6179 for (let i = 0; i < 4; i++) { 6180 const childX = (x << 1) + i % 2; 6181 const childY = (y << 1) + (i >> 1); 6182 const childZ = it.zoom + 1; 6183 stack.push({ 6184 zoom: childZ, 6185 x: childX, 6186 y: childY, 6187 wrap: it.wrap, 6188 fullyVisible 6189 }); 6190 } 6191 } 6192 return result.sort((a, b) => a.distanceSq - b.distanceSq).map((a) => a.tileID); 6193} 6194//#endregion 6195//#region src/tile/tile_manager_raster.ts 6196function isRasterType(type) { 6197 return type === "raster" || type === "image" || type === "video"; 6198} 6199/** 6200* Designate fading bases and parents using a many-to-one relationship where the lower children fade in/out 6201* with their parents. Raster shaders are not currently designed for a one-to-many fade relationship. 6202* 6203* Tiles that are candidates for fading out must be loaded and rendered tiles, as loading a tile to then 6204* fade it out would not appear smoothly. The first source of truth for tile fading always starts at the 6205* ideal tile, which continually changes on map adjustment. The state of the previously rendered ideal 6206* tile plane indicates which direction to fade each part of the newer ideal plane (with varying z). 6207* 6208* For a pitched map, the back of the map can have decreasing zooms while the front can have increasing zooms. 6209* Fade logic must therefore adapt dynamically based on the previously rendered ideal tile set. 6210*/ 6211function updateFadingTiles(inViewTiles, idealTileIDs, retain, maxFadingAncestorLevels, sourceMinZoom, sourceMaxZoom, rasterFadeDuration) { 6212 const currentTime = now(); 6213 const edgeTileIDs = getEdgeTiles(idealTileIDs); 6214 for (const idealID of idealTileIDs) { 6215 const idealTile = inViewTiles.getTileById(idealID.key); 6216 if (idealTile.fadingDirection === 0 || idealTile.fadeOpacity === 0) idealTile.resetFadeLogic(); 6217 if (updateFadingAncestor(inViewTiles, idealTile, retain, currentTime, maxFadingAncestorLevels, sourceMinZoom, rasterFadeDuration)) continue; 6218 if (updateFadingDescendents(inViewTiles, idealTile, retain, currentTime, sourceMaxZoom, rasterFadeDuration)) continue; 6219 if (updateFadingEdge(idealTile, edgeTileIDs, currentTime, rasterFadeDuration)) continue; 6220 idealTile.resetFadeLogic(); 6221 } 6222} 6223/** 6224* Many-to-one cross-fade. Set 4 ideal tiles as the fading base for a rendered parent tile 6225* as the fading parent. Here the parent is fading out and the ideal tile is fading in. 6226* 6227* Parent tile - fading out â -- Fading Parent 6228* ââââââââââââââââ¬âââââââ´ââââââââ¬âââââââââââââââ 6229* Ideal tiles - fading in â â â â -- Base Role = Incoming 6230* âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ 6231* â â â â â â â â â â â â â â â â 6232*/ 6233function updateFadingAncestor(inViewTiles, idealTile, retain, now, maxFadingAncestorLevels, sourceMinZoom, rasterFadeDuration) { 6234 if (!idealTile.hasData()) return false; 6235 const { tileID: idealID, fadingRole, fadingDirection, fadingParentID } = idealTile; 6236 if (fadingRole === 0 && fadingDirection === 1 && fadingParentID) { 6237 retain[fadingParentID.key] = fadingParentID; 6238 return true; 6239 } 6240 const minAncestorZ = Math.max(idealID.overscaledZ - maxFadingAncestorLevels, sourceMinZoom); 6241 for (let ancestorZ = idealID.overscaledZ - 1; ancestorZ >= minAncestorZ; ancestorZ--) { 6242 const ancestorID = idealID.scaledTo(ancestorZ); 6243 const ancestorTile = inViewTiles.getLoadedTile(ancestorID); 6244 if (!ancestorTile) continue; 6245 idealTile.setCrossFadeLogic({ 6246 fadingRole: 0, 6247 fadingDirection: 1, 6248 fadingParentID: ancestorTile.tileID, 6249 fadeEndTime: now + rasterFadeDuration 6250 }); 6251 ancestorTile.setCrossFadeLogic({ 6252 fadingRole: 1, 6253 fadingDirection: 0, 6254 fadeEndTime: now + rasterFadeDuration 6255 }); 6256 retain[ancestorID.key] = ancestorID; 6257 return true; 6258 } 6259 return false; 6260} 6261/** 6262* Many-to-one cross-fade. Search descendents of ideal tiles as the fading base with the ideal tile 6263* as the fading parent. Here the children are fading out and the ideal tile is fading in. 6264* 6265* â 6266* ââââââââââââââââ¬âââââââ´ââââââââ¬âââââââââââââââ 6267* Ideal tiles - fading in â â â â -- Fading Parent 6268* âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ âââââ¬ââ´ââ¬ââââ 6269* Child tiles - fading out â â â â â â â â â â â â â â â â -- Base Role = Departing 6270* 6271* Try direct children first. If none found, try grandchildren. Stops at the first generation that provides a fader. 6272*/ 6273function updateFadingDescendents(inViewTiles, idealTile, retain, now, sourceMaxZoom, rasterFadeDuration) { 6274 if (!idealTile.hasData()) return false; 6275 const idealChildren = idealTile.tileID.children(sourceMaxZoom); 6276 let hasFader = updateFadingChildren(inViewTiles, idealTile, idealChildren, retain, now, sourceMaxZoom, rasterFadeDuration); 6277 if (hasFader) return true; 6278 for (const childID of idealChildren) if (updateFadingChildren(inViewTiles, idealTile, childID.children(sourceMaxZoom), retain, now, sourceMaxZoom, rasterFadeDuration)) hasFader = true; 6279 return hasFader; 6280} 6281function updateFadingChildren(inViewTiles, idealTile, childIDs, retain, now, sourceMaxZoom, rasterFadeDuration) { 6282 if (childIDs[0].overscaledZ >= sourceMaxZoom) return false; 6283 let foundFader = false; 6284 for (const childID of childIDs) { 6285 const childTile = inViewTiles.getLoadedTile(childID); 6286 if (!childTile) continue; 6287 const { fadingRole, fadingDirection, fadingParentID } = childTile; 6288 if (fadingRole !== 0 || fadingDirection !== 0 || !fadingParentID) { 6289 childTile.setCrossFadeLogic({ 6290 fadingRole: 0, 6291 fadingDirection: 0, 6292 fadingParentID: idealTile.tileID, 6293 fadeEndTime: now + rasterFadeDuration 6294 }); 6295 idealTile.setCrossFadeLogic({ 6296 fadingRole: 1, 6297 fadingDirection: 1, 6298 fadeEndTime: now + rasterFadeDuration 6299 }); 6300 } 6301 retain[childID.key] = childID; 6302 foundFader = true; 6303 } 6304 return foundFader; 6305} 6306/** 6307* One-to-one self fading for unloaded edge tiles (for panning sideways on map). for loading tiles over gaps it feels 6308* more natural for them to fade in, however if they are already loaded/cached then there is no need to fade as map will 6309* look cohesive with no gaps. Note that draw_raster determines fade priority, as many-to-one fade supersedes edge fading. 6310*/ 6311function updateFadingEdge(idealTile, edgeTileIDs, now, rasterFadeDuration) { 6312 const idealID = idealTile.tileID; 6313 if (idealTile.selfFading) return true; 6314 if (idealTile.hasData()) return false; 6315 if (edgeTileIDs.has(idealID)) { 6316 const fadeEndTime = now + rasterFadeDuration; 6317 idealTile.setSelfFadeLogic(fadeEndTime); 6318 return true; 6319 } 6320 return false; 6321} 6322function hasRasterTransition(inViewTiles, rasterFadeDuration) { 6323 if (rasterFadeDuration <= 0) return false; 6324 const currentTime = now(); 6325 for (const tile of inViewTiles.getAllTiles()) if (tile.fadeEndTime >= currentTime) return true; 6326 return false; 6327} 6328//#endregion 6329//#region src/tile/tile_manager_raster_dem.ts 6330/** 6331* For raster terrain source, backfill DEM to eliminate visible tile boundaries 6332*/ 6333function backfillDEM(tile, inViewTiles) { 6334 const renderables = inViewTiles.getRenderableIds(); 6335 for (const borderId of renderables) { 6336 if (!tile.neighboringTiles?.[borderId]) continue; 6337 const borderTile = inViewTiles.getTileById(borderId); 6338 if (!tile.neighboringTiles[borderId].backfilled) fillBorder(tile, borderTile); 6339 if (borderTile.neighboringTiles?.[tile.tileID.key]?.backfilled) continue; 6340 fillBorder(borderTile, tile); 6341 } 6342} 6343function fillBorder(tile, borderTile) { 6344 tile.needsHillshadePrepare = true; 6345 tile.needsTerrainPrepare = true; 6346 tile.needsColorReliefPrepare = true; 6347 let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; 6348 const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; 6349 const dim = Math.pow(2, tile.tileID.canonical.z); 6350 const borderId = borderTile.tileID.key; 6351 if (dx === 0 && dy === 0) return; 6352 if (Math.abs(dy) > 1) return; 6353 if (Math.abs(dx) > 1) { 6354 if (Math.abs(dx + dim) === 1) dx += dim; 6355 else if (Math.abs(dx - dim) === 1) dx -= dim; 6356 } 6357 if (!borderTile.dem || !tile.dem) return; 6358 tile.dem.backfillBorder(borderTile.dem, dx, dy); 6359 if (tile.neighboringTiles?.[borderId]) tile.neighboringTiles[borderId].backfilled = true; 6360} 6361//#endregion 6362//#region src/tile/tile_manager_in_view_tiles.ts 6363var InViewTiles = class { 6364 constructor() { 6365 this._tiles = {}; 6366 } 6367 handleWrapJump(wrapDelta) { 6368 const tiles = {}; 6369 for (const id in this._tiles) { 6370 const tile = this._tiles[id]; 6371 tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); 6372 tiles[tile.tileID.key] = tile; 6373 } 6374 this._tiles = tiles; 6375 } 6376 setFeatureState(featuresChanged, painter, revision) { 6377 for (const id in this._tiles) this._tiles[id].setFeatureState(featuresChanged, painter, revision); 6378 } 6379 getAllTiles() { 6380 return Object.values(this._tiles); 6381 } 6382 getAllIds(sorted = false) { 6383 if (sorted) return Object.values(this._tiles).map((tile) => tile.tileID).sort(compareTileId).map((id) => id.key); 6384 return Object.keys(this._tiles); 6385 } 6386 getTileById(key) { 6387 return this._tiles[key]; 6388 } 6389 setTile(key, tile) { 6390 this._tiles[key] = tile; 6391 } 6392 deleteTileById(key) { 6393 delete this._tiles[key]; 6394 } 6395 /** 6396 * Get a currently loaded tile. 6397 * - a cached tile is not a loaded tile 6398 * @returns the tile if it's in view and had data, null otherwise. 6399 */ 6400 getLoadedTile(tileID) { 6401 const tile = this.getTileById(tileID.key); 6402 if (tile?.hasData()) return tile; 6403 return null; 6404 } 6405 isIdRenderable(id, symbolLayer = false) { 6406 return this.getTileById(id)?.isRenderable(symbolLayer); 6407 } 6408 getRenderableIds(bearingInRadians = 0, symbolLayer) { 6409 const renderables = []; 6410 for (const id of this.getAllIds()) if (this.isIdRenderable(id, symbolLayer)) renderables.push(this.getTileById(id)); 6411 if (symbolLayer) return renderables.sort((a_, b_) => { 6412 const a = a_.tileID; 6413 const b = b_.tileID; 6414 const rotatedA = new Point(a.canonical.x, a.canonical.y)._rotate(-bearingInRadians); 6415 const rotatedB = new Point(b.canonical.x, b.canonical.y)._rotate(-bearingInRadians); 6416 return a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || rotatedB.x - rotatedA.x; 6417 }).map((tile) => tile.tileID.key); 6418 return renderables.map((tile) => tile.tileID).sort(compareTileId).map((id) => id.key); 6419 } 6420}; 6421//#endregion 6422//#region src/tile/tile_manager.ts 6423/** 6424* @internal 6425* `TileManager` is responsible for 6426* 6427* - creating an instance of `Source` 6428* - caching tiles loaded from an instance of `Source` 6429* - handling incoming source data events events from `Map` and coordinating updates 6430* - providing the current renderable tile coordinates to the `Painter` 6431* - loading the tiles needed to render a given viewport 6432* - retaining the tiles needed as substitutes for pending loading tiles 6433* - retaining the tiles needed for fading between parents and children (for raster sources) 6434* - reloading tiles when source data or dependencies change 6435* - handling tile expiration and refresh timers 6436* - unloading cached tiles not needed to render a given viewport 6437* - managing tile state and feature state 6438*/ 6439var TileManager = class TileManager extends Evented { 6440 static { 6441 this.maxUnderzooming = 10; 6442 } 6443 static { 6444 this.maxOverzooming = 3; 6445 } 6446 constructor(id, options, dispatcher) { 6447 super(); 6448 this._maxContentElevationSeen = 0; 6449 this.id = id; 6450 this.dispatcher = dispatcher; 6451 this.on("data", (e) => { 6452 this._dataHandler(e); 6453 }); 6454 this.on("dataloading", () => { 6455 this._sourceErrored = false; 6456 }); 6457 this.on("error", () => { 6458 this._sourceErrored = this._source.loaded(); 6459 }); 6460 this._source = create(id, options, dispatcher, this); 6461 this._inViewTiles = new InViewTiles(); 6462 this._outOfViewCache = new TileCache(0, (tile) => this._unloadTile(tile)); 6463 this._timers = {}; 6464 this._maxTileCacheSize = null; 6465 this._maxTileCacheZoomLevels = null; 6466 this._rasterFadeDuration = 0; 6467 this._maxFadingAncestorLevels = 5; 6468 this._state = new SourceFeatureState(); 6469 this._didEmitContent = false; 6470 this._updated = false; 6471 } 6472 onAdd(map) { 6473 this.map = map; 6474 this._maxTileCacheSize = map ? map._maxTileCacheSize : null; 6475 this._maxTileCacheZoomLevels = map ? map._maxTileCacheZoomLevels : null; 6476 if (this._source?.onAdd) this._source.onAdd(map); 6477 } 6478 onRemove(map) { 6479 for (const tile of this._inViewTiles.getAllTiles()) tile.unloadVectorData(); 6480 this.clearTiles(); 6481 if (this._source?.onRemove) this._source.onRemove(map); 6482 this._inViewTiles = new InViewTiles(); 6483 } 6484 /** 6485 * Return true if no tile data is pending, tiles will not change unless 6486 * an additional API call is received. 6487 */ 6488 loaded() { 6489 if (this._sourceErrored) return true; 6490 if (!this._sourceLoaded) return false; 6491 if (!this._source.loaded()) return false; 6492 if ((this.used !== void 0 || this.usedForTerrain !== void 0) && !this.used && !this.usedForTerrain) return true; 6493 if (!this._updated) return false; 6494 for (const tile of this._inViewTiles.getAllTiles()) if (tile.state !== "loaded" && tile.state !== "errored") return false; 6495 return true; 6496 } 6497 getSource() { 6498 return this._source; 6499 } 6500 getState() { 6501 return this._state; 6502 } 6503 pause() { 6504 this._paused = true; 6505 } 6506 resume() { 6507 if (!this._paused) return; 6508 const shouldReload = this._shouldReloadOnResume; 6509 this._paused = false; 6510 this._shouldReloadOnResume = false; 6511 if (shouldReload) this.reload(); 6512 if (this.transform) this.update(this.transform, this.terrain); 6513 } 6514 async _loadTile(tile, id, state, hadData) { 6515 try { 6516 const result = await this._source.loadTile(tile); 6517 this._tileLoaded(tile, id, state, hadData, result); 6518 } catch (err) { 6519 tile.state = "errored"; 6520 if (err.status !== 404) this._source.fire(new ErrorEvent(ensureError(err), { tile })); 6521 else this.update(this.transform, this.terrain); 6522 } 6523 } 6524 _unloadTile(tile) { 6525 if (this._source.unloadTile) this._source.unloadTile(tile); 6526 } 6527 _abortTile(tile) { 6528 if (this._source.abortTile) this._source.abortTile(tile); 6529 this._source.fire(new MapSourceDataEvent("dataabort", { 6530 tile, 6531 coord: tile.tileID 6532 })); 6533 } 6534 serialize() { 6535 return this._source.serialize(); 6536 } 6537 prepare(context) { 6538 if (this._source.prepare) this._source.prepare(); 6539 this._state.coalesceChanges(this._inViewTiles, this.map ? this.map.painter : null); 6540 for (const tile of this._inViewTiles.getAllTiles()) { 6541 tile.upload(context); 6542 tile.prepare(this.map.style.imageManager); 6543 } 6544 } 6545 /** 6546 * Return all tile ids ordered with z-order, and cast to numbers 6547 */ 6548 getIds() { 6549 return this._inViewTiles.getAllIds(true); 6550 } 6551 getRenderableIds(symbolLayer) { 6552 return this._inViewTiles.getRenderableIds(this.transform?.bearingInRadians, symbolLayer); 6553 } 6554 hasRenderableParent(tileID) { 6555 const parentZ = tileID.overscaledZ - 1; 6556 if (parentZ >= this._source.minzoom) { 6557 const parentTile = this.getLoadedTile(tileID.scaledTo(parentZ)); 6558 if (parentTile) return this._inViewTiles.isIdRenderable(parentTile.tileID.key); 6559 } 6560 return false; 6561 } 6562 /** 6563 * Reload tiles based on the current state of the source. 6564 * @param sourceDataChanged - If `true`, reload all tiles using a state of 'expired' (errored tiles use 'loading' since they have nothing to show yet), otherwise reload only non-errored tiles using state of 'reloading'. 6565 * @param shouldReloadTileOptions - Set of options associated with a `MapSourceDataChangedEvent` that can be passed back to the associated `Source` determine whether a tile should be reloaded. 6566 */ 6567 reload(sourceDataChanged, shouldReloadTileOptions = void 0) { 6568 if (this._paused) { 6569 this._shouldReloadOnResume = true; 6570 return; 6571 } 6572 this._outOfViewCache.reset(); 6573 for (const id of this._inViewTiles.getAllIds()) { 6574 const tile = this._inViewTiles.getTileById(id); 6575 if (shouldReloadTileOptions && !this._source.shouldReloadTile(tile, shouldReloadTileOptions)) continue; 6576 else if (sourceDataChanged) this._reloadTile(id, tile.state === "errored" ? "loading" : "expired"); 6577 else if (tile.state !== "errored") this._reloadTile(id, "reloading"); 6578 } 6579 } 6580 async _reloadTile(id, state) { 6581 const tile = this._inViewTiles.getTileById(id); 6582 if (!tile) return; 6583 const hadData = tile.hasData(); 6584 if (tile.state !== "loading") tile.state = state; 6585 await this._loadTile(tile, id, state, hadData); 6586 } 6587 _tileLoaded(tile, id, previousState, hadData, result) { 6588 if (!hadData) { 6589 tile.timeAdded = now(); 6590 if (tile.selfFading) tile.fadeEndTime = tile.timeAdded + this._rasterFadeDuration; 6591 } 6592 if (previousState === "expired") tile.refreshedUponExpiration = true; 6593 this._setTileReloadTimer(id, tile); 6594 if (result?.unmodified) return; 6595 if (this.getSource().type === "raster-dem" && tile.dem) backfillDEM(tile, this._inViewTiles); 6596 tile.featureStateRevision = -1; 6597 this._state.initializeTileState(tile, this.map ? this.map.painter : null); 6598 if (!tile.aborted) this._source.fire(new MapSourceDataEvent("data", { 6599 tile, 6600 coord: tile.tileID 6601 })); 6602 } 6603 /** 6604 * Get a specific tile by TileID 6605 */ 6606 getTile(tileID) { 6607 return this.getTileByID(tileID.key); 6608 } 6609 /** 6610 * Get a specific tile by id 6611 */ 6612 getTileByID(id) { 6613 return this._inViewTiles.getTileById(id); 6614 } 6615 /** 6616 * Retain the uppermost loaded children of each provided target tile, within a variable covering zoom range. 6617 * 6618 * On pitched maps, different parts of the screen show different zoom levels simultaneously. 6619 * Ideal tiles are generated using coveringTiles() above, which returns the ideal tile set for 6620 * the current pitched plane, which can carry tiles of varying zooms (overscaledZ). 6621 * See: https://maplibre.org/maplibre-gl-js/docs/examples/level-of-detail-control/ 6622 * 6623 * A fixed `maxCoveringZoom` on a pitched map would incorrectly intersect with some 6624 * ideal tiles and cause distant high-pitch tiles to skip their uppermost children. 6625 * 6626 * To solve this, we calculate the max covering zoom for each ideal tile separately using its 6627 * `overscaledZ`. This effectively makes the "max covering zoom plane" parallel to the 6628 * "ideal tile plane," ensuring that we correctly capture the uppermost children 6629 * of each ideal tile across the pitched view. 6630 * 6631 * Analogy: imagine two sheets of paper in 3D space: 6632 * - one sheet = ideal tiles at varying overscaledZ 6633 * - the second sheet = maxCoveringZoom 6634 * 6635 * @param retainTileMap - this parameters will be updated with the child tiles to keep 6636 * @param idealTilesWithoutData - which of the ideal tiles currently does not have loaded data 6637 * @return a set of tiles that need to be loaded 6638 */ 6639 _retainLoadedChildren(retainTileMap, idealTilesWithoutData) { 6640 const loadedDescendents = this._getLoadedDescendents(idealTilesWithoutData); 6641 const incomplete = /* @__PURE__ */ new Set(); 6642 for (const targetID of idealTilesWithoutData) { 6643 const descendents = loadedDescendents[targetID.key]; 6644 if (!descendents?.length) { 6645 incomplete.add(targetID); 6646 continue; 6647 } 6648 const maxCoveringZoom = targetID.overscaledZ + TileManager.maxOverzooming; 6649 const candidates = descendents.filter((t) => t.tileID.overscaledZ <= maxCoveringZoom); 6650 if (!candidates.length) { 6651 incomplete.add(targetID); 6652 continue; 6653 } 6654 const topZoom = Math.min(...candidates.map((t) => t.tileID.overscaledZ)); 6655 const topIDs = candidates.filter((t) => t.tileID.overscaledZ === topZoom).map((t) => t.tileID); 6656 for (const tileID of topIDs) retainTileMap[tileID.key] = tileID; 6657 if (!this._areDescendentsComplete(topIDs, topZoom, targetID.overscaledZ)) incomplete.add(targetID); 6658 } 6659 return incomplete; 6660 } 6661 /** 6662 * Return dictionary of qualified loaded descendents for each provided target tile id 6663 */ 6664 _getLoadedDescendents(targetTileIDs) { 6665 const loadedDescendents = {}; 6666 for (const tile of this._inViewTiles.getAllTiles().filter((tile) => tile.hasData())) for (const targetID of targetTileIDs) if (tile.tileID.isChildOf(targetID)) { 6667 loadedDescendents[targetID.key] ||= []; 6668 loadedDescendents[targetID.key].push(tile); 6669 } 6670 return loadedDescendents; 6671 } 6672 /** 6673 * Determine if tile ids fully cover the current generation. 6674 * - 1st generation: need 4 children or 1 overscaled child 6675 * - 2nd generation: need 16 children or 1 overscaled child 6676 */ 6677 _areDescendentsComplete(generationIDs, generationZ, ancestorZ) { 6678 if (generationIDs.length === 1 && generationIDs[0].isOverscaled()) return generationIDs[0].overscaledZ === generationZ; 6679 else return Math.pow(4, generationZ - ancestorZ) === generationIDs.length; 6680 } 6681 /** 6682 * Get a currently loaded tile. 6683 * - a cached tile is not a loaded tile 6684 * @returns the tile if it's in view and had data, null otherwise. 6685 */ 6686 getLoadedTile(tileID) { 6687 return this._inViewTiles.getLoadedTile(tileID); 6688 } 6689 /** 6690 * Resizes the tile cache based on the current viewport's size 6691 * or the maxTileCacheSize option passed during map creation 6692 * 6693 * Larger viewports use more tiles and need larger caches. Larger viewports 6694 * are more likely to be found on devices with more memory and on pages where 6695 * the map is more important. 6696 */ 6697 updateCacheSize(transform) { 6698 const approxTilesInView = (Math.ceil(transform.width / this._source.tileSize) + 1) * (Math.ceil(transform.height / this._source.tileSize) + 1); 6699 const commonZoomRange = this._maxTileCacheZoomLevels === null ? config.MAX_TILE_CACHE_ZOOM_LEVELS : this._maxTileCacheZoomLevels; 6700 const viewDependentMaxSize = Math.floor(approxTilesInView * commonZoomRange); 6701 const maxSize = typeof this._maxTileCacheSize === "number" ? Math.min(this._maxTileCacheSize, viewDependentMaxSize) : viewDependentMaxSize; 6702 this._outOfViewCache.setMaxSize(maxSize); 6703 } 6704 handleWrapJump(lng) { 6705 const worldDifference = (lng - (this._prevLng === void 0 ? lng : this._prevLng)) / 360; 6706 const wrapDelta = Math.round(worldDifference); 6707 this._prevLng = lng; 6708 if (wrapDelta) { 6709 this._inViewTiles.handleWrapJump(wrapDelta); 6710 this._resetTileReloadTimers(); 6711 } 6712 } 6713 /** 6714 * The highest elevation this source's symbols may reach, in meters: `symbol-height-offset` 6715 * constants read from the visible symbol layers, data-driven maxima tracked by the loaded 6716 * buckets at layout time. Only data-driven layers scan the loaded tiles, so styles without 6717 * them pay nothing beyond the layer loop. The value is a high-water mark: a maximum seen once is kept even 6718 * after its tile unloads, otherwise dropping the tile would also drop the reason to keep it, 6719 * and the tile could not come back while its content is still visible. The mark resets with 6720 * the tiles in `clearTiles`. 6721 */ 6722 _updateMaxContentElevation() { 6723 let maxElevation = this._maxContentElevationSeen; 6724 const layers = this.map?.style?._layers; 6725 if (!layers) return maxElevation; 6726 const tiles = this._inViewTiles.getAllTiles(); 6727 for (const layerId in layers) { 6728 const layer = layers[layerId]; 6729 if (layer.type !== "symbol" || layer.source !== this.id || layer.isHidden(this.transform.zoom)) continue; 6730 const symbolLayer = layer; 6731 if (!symbolLayer.layout) continue; 6732 const heightOffset = symbolLayer.layout.get("symbol-height-offset"); 6733 maxElevation = Math.max(maxElevation, heightOffset.constantOr(0)); 6734 if (heightOffset.isConstant()) continue; 6735 for (const tile of tiles) { 6736 const bucket = tile.getBucket(layer); 6737 if (bucket && bucket.maxHeightOffset > maxElevation) maxElevation = bucket.maxHeightOffset; 6738 } 6739 } 6740 this._maxContentElevationSeen = maxElevation; 6741 return maxElevation; 6742 } 6743 /** 6744 * Removes tiles that are outside the viewport and adds new tiles that 6745 * are inside the viewport. 6746 */ 6747 update(transform, terrain) { 6748 if (!this._sourceLoaded || this._paused) return; 6749 this.transform = transform; 6750 this.terrain = terrain; 6751 this.updateCacheSize(transform); 6752 this.handleWrapJump(this.transform.center.lng); 6753 let idealTileIDs; 6754 if (!this.used && !this.usedForTerrain) idealTileIDs = []; 6755 else if (this._source.tileID) idealTileIDs = transform.getVisibleUnwrappedCoordinates(this._source.tileID).map((unwrapped) => new OverscaledTileID(unwrapped.canonical.z, unwrapped.wrap, unwrapped.canonical.z, unwrapped.canonical.x, unwrapped.canonical.y)); 6756 else { 6757 idealTileIDs = coveringTiles(transform, { 6758 tileSize: this.usedForTerrain ? this.tileSize : this._source.tileSize, 6759 minzoom: this._source.minzoom, 6760 maxzoom: this._source.type === "vector" && this.map._zoomLevelsToOverscale !== void 0 ? Math.max(this._source.maxzoom, transform.maxZoom - this.map._zoomLevelsToOverscale) : this._source.maxzoom, 6761 roundZoom: this.usedForTerrain ? false : this._source.roundZoom, 6762 reparseOverscaled: this._source.reparseOverscaled, 6763 terrain, 6764 calculateTileZoom: this._source.calculateTileZoom, 6765 maxContentElevation: this._updateMaxContentElevation() 6766 }); 6767 if (this._source.hasTile) idealTileIDs = idealTileIDs.filter((coord) => this._source.hasTile(coord)); 6768 } 6769 if (this.usedForTerrain) idealTileIDs = this._addTerrainIdealTiles(idealTileIDs); 6770 const noPendingDataEmissions = idealTileIDs.length === 0 && !this._updated && this._didEmitContent; 6771 this._updated = true; 6772 if (noPendingDataEmissions) this.fire(new MapSourceDataEvent("data", { 6773 sourceDataType: "idle", 6774 sourceId: this.id 6775 })); 6776 const zoom = coveringZoomLevel(transform, this._source); 6777 const retain = this._updateRetainedTiles(idealTileIDs, zoom); 6778 const isRaster = isRasterType(this._source.type); 6779 if (isRaster && this._rasterFadeDuration > 0 && !terrain) updateFadingTiles(this._inViewTiles, idealTileIDs, retain, this._maxFadingAncestorLevels, this._source.minzoom, this._source.maxzoom, this._rasterFadeDuration); 6780 if (isRaster) this._cleanUpRasterTiles(retain); 6781 else this._cleanUpVectorTiles(retain); 6782 } 6783 /** 6784 * Remove raster tiles that are no longer retained 6785 */ 6786 _cleanUpRasterTiles(retain) { 6787 for (const id of this._inViewTiles.getAllIds()) if (!retain[id]) this._removeTile(id); 6788 } 6789 /** 6790 * Remove vector tiles that are no longer retained and also not needed for symbol fading 6791 */ 6792 _cleanUpVectorTiles(retain) { 6793 for (const id of this._inViewTiles.getAllIds()) { 6794 const tile = this._inViewTiles.getTileById(id); 6795 if (retain[id]) { 6796 tile.clearSymbolFadeHold(); 6797 continue; 6798 } 6799 if (!tile.hasSymbolBuckets) { 6800 this._removeTile(id); 6801 continue; 6802 } 6803 if (!tile.holdingForSymbolFade()) tile.setSymbolHoldDuration(this.map._fadeDuration); 6804 else if (tile.symbolFadeFinished()) this._removeTile(id); 6805 } 6806 } 6807 /** 6808 * Add ideal tiles needed for 3D terrain rendering 6809 */ 6810 _addTerrainIdealTiles(idealTileIDs) { 6811 const ancestors = []; 6812 for (const tileID of idealTileIDs) if (tileID.canonical.z > this._source.minzoom) { 6813 const parent = tileID.scaledTo(tileID.canonical.z - 1); 6814 ancestors.push(parent); 6815 const parent2 = tileID.scaledTo(Math.max(this._source.minzoom, Math.min(tileID.canonical.z, 5))); 6816 ancestors.push(parent2); 6817 } 6818 return idealTileIDs.concat(ancestors); 6819 } 6820 releaseSymbolFadeTiles() { 6821 for (const id of this._inViewTiles.getAllIds()) if (this._inViewTiles.getTileById(id).holdingForSymbolFade()) this._removeTile(id); 6822 } 6823 /** 6824 * Set tiles to be retained on update of the source. For ideal tiles that do not have data, retain their loaded 6825 * children so they can be displayed as substitutes pending load of each ideal tile (to reduce flickering). 6826 * If no loaded children are available, fallback to seeking loaded parents as an alternative substitute. 6827 */ 6828 _updateRetainedTiles(idealTileIDs, zoom) { 6829 const idealTilesWithoutData = /* @__PURE__ */ new Set(); 6830 for (const idealID of idealTileIDs) if (!this._addTile(idealID).hasData()) idealTilesWithoutData.add(idealID); 6831 const retainTileMap = idealTileIDs.reduce((acc, t) => { 6832 acc[t.key] = t; 6833 return acc; 6834 }, {}); 6835 const tileIdsWithoutData = this._retainLoadedChildren(retainTileMap, idealTilesWithoutData); 6836 const checked = {}; 6837 const minCoveringZoom = Math.max(zoom - TileManager.maxUnderzooming, this._source.minzoom); 6838 for (const tileID of tileIdsWithoutData) { 6839 let tile = this._inViewTiles.getTileById(tileID.key); 6840 let parentWasRequested = tile?.wasRequested(); 6841 for (let overscaledZ = tileID.overscaledZ - 1; overscaledZ >= minCoveringZoom; --overscaledZ) { 6842 const parentId = tileID.scaledTo(overscaledZ); 6843 if (checked[parentId.key]) break; 6844 checked[parentId.key] = true; 6845 tile = this.getTile(parentId); 6846 if (!tile && parentWasRequested) tile = this._addTile(parentId); 6847 if (tile) { 6848 const hasData = tile.hasData(); 6849 if (hasData || !this.map?.cancelPendingTileRequestsWhileZooming || parentWasRequested) retainTileMap[parentId.key] = parentId; 6850 parentWasRequested = tile.wasRequested(); 6851 if (hasData) break; 6852 } 6853 } 6854 } 6855 return retainTileMap; 6856 } 6857 /** 6858 * Add a tile, given its coordinate, to the pyramid. 6859 */ 6860 _addTile(tileID) { 6861 let tile = this._inViewTiles.getTileById(tileID.key); 6862 if (tile) return tile; 6863 tile = this._outOfViewCache.getAndRemove(tileID); 6864 if (tile) { 6865 tile.resetFadeLogic(); 6866 this._setTileReloadTimer(tileID.key, tile); 6867 tile.tileID = tileID; 6868 this._state.initializeTileState(tile, this.map ? this.map.painter : null); 6869 } 6870 const cached = tile; 6871 if (!tile) { 6872 tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor()); 6873 this._loadTile(tile, tileID.key, tile.state, false); 6874 } 6875 tile.uses++; 6876 this._inViewTiles.setTile(tileID.key, tile); 6877 if (!cached) this._source.fire(new MapSourceDataEvent("dataloading", { 6878 tile, 6879 coord: tile.tileID 6880 })); 6881 return tile; 6882 } 6883 /** 6884 * Set a timeout to reload the tile after it expires 6885 */ 6886 _setTileReloadTimer(id, tile) { 6887 this._clearTileReloadTimer(id); 6888 const expiryTimeout = tile.getExpiryTimeout(); 6889 if (expiryTimeout) { 6890 const reload = () => { 6891 this._reloadTile(id, "expired"); 6892 delete this._timers[id]; 6893 }; 6894 this._timers[id] = setTimeout(reload, expiryTimeout); 6895 } 6896 } 6897 _clearTileReloadTimer(id) { 6898 const timeout = this._timers[id]; 6899 if (timeout) { 6900 clearTimeout(timeout); 6901 delete this._timers[id]; 6902 } 6903 } 6904 _resetTileReloadTimers() { 6905 for (const id in this._timers) { 6906 clearTimeout(this._timers[id]); 6907 delete this._timers[id]; 6908 } 6909 for (const id of this._inViewTiles.getAllIds()) { 6910 const tile = this._inViewTiles.getTileById(id); 6911 this._setTileReloadTimer(id, tile); 6912 } 6913 } 6914 /** 6915 * Reload any currently renderable tiles that are match one of the incoming `tileId` x/y/z 6916 */ 6917 refreshTiles(tileIds) { 6918 for (const id of this._inViewTiles.getAllIds()) { 6919 const tile = this._inViewTiles.getTileById(id); 6920 if (!this._inViewTiles.isIdRenderable(id) && tile.state != "errored") continue; 6921 if (tileIds.some((tid) => tid.equals(tile.tileID.canonical))) this._reloadTile(id, "expired"); 6922 } 6923 } 6924 /** 6925 * Remove a tile, given its id, from the pyramid 6926 */ 6927 _removeTile(id) { 6928 const tile = this._inViewTiles.getTileById(id); 6929 if (!tile) return; 6930 tile.uses--; 6931 this._inViewTiles.deleteTileById(id); 6932 this._clearTileReloadTimer(id); 6933 if (tile.uses > 0) return; 6934 if (tile.hasData() && tile.state !== "reloading") this._outOfViewCache.add(tile.tileID, tile, tile.getExpiryTimeout()); 6935 else { 6936 tile.aborted = true; 6937 this._abortTile(tile); 6938 this._unloadTile(tile); 6939 } 6940 } 6941 /** @internal 6942 * Handles incoming source data messages (i.e. after the source has been updated via a worker that has fired 6943 * to map.ts data event). For sources with mutable data, the 'content' event fires when the underlying data 6944 * to a source has changed. (i.e. GeoJSONSource.setData and ImageSource.setCoordinates) 6945 */ 6946 _dataHandler(e) { 6947 if (e.dataType !== "source") return; 6948 if (e.sourceDataType === "metadata") { 6949 this._sourceLoaded = true; 6950 return; 6951 } 6952 if (e.sourceDataType !== "content" || !this._sourceLoaded || this._paused) return; 6953 this.reload(e.sourceDataChanged, e.shouldReloadTileOptions); 6954 if (this.transform) this.update(this.transform, this.terrain); 6955 this._didEmitContent = true; 6956 } 6957 /** 6958 * Forgets the recorded maximum content elevation, so the next update recomputes it from the 6959 * current layers and loaded tiles. Called when a symbol layer is removed, since the removed 6960 * layer's heights would otherwise keep expanding tile coverage. 6961 */ 6962 resetMaxContentElevation() { 6963 this._maxContentElevationSeen = 0; 6964 } 6965 /** 6966 * Remove all tiles from this pyramid 6967 */ 6968 clearTiles() { 6969 this._shouldReloadOnResume = false; 6970 this._paused = false; 6971 this.resetMaxContentElevation(); 6972 for (const id of this._inViewTiles.getAllIds()) this._removeTile(id); 6973 this._outOfViewCache.reset(); 6974 } 6975 /** 6976 * Search through our current tiles and attempt to find the tiles that 6977 * cover the given bounds. 6978 * @param pointQueryGeometry - coordinates of the corners of bounding rectangle 6979 * @returns result items have `{tile, minX, maxX, minY, maxY}`, where min/max bounding values are the given bounds transformed in into the coordinate space of this tile. 6980 */ 6981 tilesIn(pointQueryGeometry, maxPitchScaleFactor, has3DLayer) { 6982 const tileResults = []; 6983 const transform = this.transform; 6984 if (!transform) return tileResults; 6985 const allowWorldCopies = transform.getCoveringTilesDetailsProvider().allowWorldCopies(); 6986 const cameraPointQueryGeometry = has3DLayer ? transform.getCameraQueryGeometry(pointQueryGeometry) : pointQueryGeometry; 6987 const project = (point) => transform.screenPointToMercatorCoordinate(point, this.terrain); 6988 const queryGeometry = this.transformBbox(pointQueryGeometry, project, !allowWorldCopies); 6989 const cameraQueryGeometry = this.transformBbox(cameraPointQueryGeometry, project, !allowWorldCopies); 6990 const ids = this.getIds(); 6991 const cameraBounds = Bounds.fromPoints(cameraQueryGeometry); 6992 for (const id of ids) { 6993 const tile = this._inViewTiles.getTileById(id); 6994 if (tile.holdingForSymbolFade()) continue; 6995 const tileIDs = allowWorldCopies ? [tile.tileID] : [tile.tileID.unwrapTo(-1), tile.tileID.unwrapTo(0)]; 6996 const scale = Math.pow(2, transform.zoom - tile.tileID.overscaledZ); 6997 const queryPadding = maxPitchScaleFactor * tile.queryPadding * EXTENT / tile.tileSize / scale; 6998 for (const tileID of tileIDs) { 6999 const tileSpaceBounds = cameraBounds.map((point) => tileID.getTilePoint(new MercatorCoordinate(point.x, point.y))); 7000 tileSpaceBounds.expandBy(queryPadding); 7001 if (tileSpaceBounds.intersects(EXTENT_BOUNDS)) { 7002 const tileSpaceQueryGeometry = queryGeometry.map((c) => tileID.getTilePoint(c)); 7003 const tileSpaceCameraQueryGeometry = cameraQueryGeometry.map((c) => tileID.getTilePoint(c)); 7004 tileResults.push({ 7005 tile, 7006 tileID: allowWorldCopies ? tileID : tileID.unwrapTo(0), 7007 queryGeometry: tileSpaceQueryGeometry, 7008 cameraQueryGeometry: tileSpaceCameraQueryGeometry, 7009 scale 7010 }); 7011 } 7012 } 7013 } 7014 return tileResults; 7015 } 7016 transformBbox(geom, project, checkWrap) { 7017 let transformed = geom.map(project); 7018 if (checkWrap) { 7019 const bounds = Bounds.fromPoints(geom); 7020 bounds.shrinkBy(Math.min(bounds.width(), bounds.height()) * .001); 7021 const projected = bounds.map(project); 7022 if (!Bounds.fromPoints(transformed).covers(projected)) transformed = transformed.map((coord) => coord.x > .5 ? new MercatorCoordinate(coord.x - 1, coord.y, coord.z) : coord); 7023 } 7024 return transformed; 7025 } 7026 getVisibleCoordinates(symbolLayer) { 7027 const coords = this.getRenderableIds(symbolLayer).map((id) => this._inViewTiles.getTileById(id).tileID); 7028 if (this.transform) this.transform.populateCache(coords); 7029 return coords; 7030 } 7031 hasTransition() { 7032 if (this._source.hasTransition()) return true; 7033 return isRasterType(this._source.type) && hasRasterTransition(this._inViewTiles, this._rasterFadeDuration); 7034 } 7035 setRasterFadeDuration(fadeDuration) { 7036 this._rasterFadeDuration = fadeDuration; 7037 } 7038 /** 7039 * Set the value of a particular state for a feature 7040 */ 7041 setFeatureState(sourceLayer, featureId, state) { 7042 sourceLayer ||= GEOJSON_TILE_LAYER_NAME; 7043 this._state.updateState(sourceLayer, featureId, state); 7044 } 7045 /** 7046 * Resets the value of a particular state key for a feature 7047 */ 7048 removeFeatureState(sourceLayer, featureId, key) { 7049 sourceLayer ||= GEOJSON_TILE_LAYER_NAME; 7050 this._state.removeFeatureState(sourceLayer, featureId, key); 7051 } 7052 /** 7053 * Get the entire state object for a feature 7054 */ 7055 getFeatureState(sourceLayer, featureId) { 7056 sourceLayer ||= GEOJSON_TILE_LAYER_NAME; 7057 return this._state.getState(sourceLayer, featureId); 7058 } 7059 /** 7060 * Sets the set of keys that the tile depends on. This allows tiles to 7061 * be reloaded when their dependencies change. 7062 */ 7063 setDependencies(tileKey, namespace, dependencies) { 7064 const tile = this._inViewTiles.getTileById(tileKey); 7065 if (tile) tile.setDependencies(namespace, dependencies); 7066 } 7067 /** 7068 * Reloads all tiles that depend on the given keys. 7069 */ 7070 reloadTilesForDependencies(namespaces, keys) { 7071 for (const id of this._inViewTiles.getAllIds()) if (this._inViewTiles.getTileById(id).hasDependency(namespaces, keys)) this._reloadTile(id, "reloading"); 7072 this._outOfViewCache.filter((tile) => !tile.hasDependency(namespaces, keys)); 7073 } 7074 areTilesLoaded() { 7075 for (const tile of this._inViewTiles.getAllTiles()) if (!(tile.state === "loaded" || tile.state === "errored")) return false; 7076 return true; 7077 } 7078}; 7079//#endregion 7080//#region src/symbol/path_interpolator.ts 7081var PathInterpolator = class { 7082 constructor(points_, padding_) { 7083 this.reset(points_, padding_); 7084 } 7085 reset(points_, padding_) { 7086 this.points = points_ || []; 7087 this._distances = [0]; 7088 for (let i = 1; i < this.points.length; i++) this._distances[i] = this._distances[i - 1] + this.points[i].dist(this.points[i - 1]); 7089 this.length = this._distances[this._distances.length - 1]; 7090 this.padding = Math.min(padding_ || 0, this.length * .5); 7091 this.paddedLength = this.length - this.padding * 2; 7092 } 7093 lerp(t) { 7094 if (this.points.length === 1) return this.points[0]; 7095 t = clamp(t, 0, 1); 7096 let currentIndex = 1; 7097 let distOfCurrentIdx = this._distances[currentIndex]; 7098 const distToTarget = t * this.paddedLength + this.padding; 7099 while (distOfCurrentIdx < distToTarget && currentIndex < this._distances.length) distOfCurrentIdx = this._distances[++currentIndex]; 7100 const idxOfPrevPoint = currentIndex - 1; 7101 const distOfPrevIdx = this._distances[idxOfPrevPoint]; 7102 const segmentLength = distOfCurrentIdx - distOfPrevIdx; 7103 const segmentT = segmentLength > 0 ? (distToTarget - distOfPrevIdx) / segmentLength : 0; 7104 return this.points[idxOfPrevPoint].mult(1 - segmentT).add(this.points[currentIndex].mult(segmentT)); 7105 } 7106}; 7107//#endregion 7108//#region src/symbol/symbol_elevation.ts 7109/** 7110* Resolves the elevation of a symbol at a tile coordinate, combining the terrain elevation with the 7111* symbol's `symbol-height-offset`. `symbol-height-anchor` selects the datum the offset is measured 7112* from: the terrain surface below the symbol (`ground`) or the zero elevation datum (`absolute`), 7113* in which case the terrain below the symbol is ignored. 7114* 7115* Call this where the elevation is needed, so that `getElevation` keeps returning the plain terrain 7116* elevation and never has to be substituted with a wrapper. 7117* 7118* @param getElevation - the terrain elevation function, absent when the map has no terrain 7119* @param x - the tile x coordinate to evaluate the elevation at 7120* @param y - the tile y coordinate to evaluate the elevation at 7121* @param heightOffset - the evaluated `symbol-height-offset` of the symbol, in meters 7122* @param heightAnchorGround - whether `symbol-height-anchor` is `ground` 7123* @returns the elevation in meters, or undefined when the symbol sits at the zero elevation datum, 7124* which lets the projection take its fast path that ignores the z coordinate 7125*/ 7126function getSymbolElevation(getElevation, x, y, heightOffset, heightAnchorGround) { 7127 if (!heightAnchorGround) return heightOffset; 7128 if (!getElevation) return heightOffset !== 0 ? heightOffset : void 0; 7129 return getElevation(x, y) + heightOffset; 7130} 7131//#endregion 7132//#region src/symbol/grid_index.ts 7133function overlapAllowed(overlapA, overlapB) { 7134 let allowed = true; 7135 if (overlapA === "always") {} else if (overlapA === "never" || overlapB === "never") allowed = false; 7136 return allowed; 7137} 7138/** 7139* @internal 7140* GridIndex is a data structure for testing the intersection of 7141* circles and rectangles in a 2d plane. 7142* It is optimized for rapid insertion and querying. 7143* GridIndex splits the plane into a set of "cells" and keeps track 7144* of which geometries intersect with each cell. At query time, 7145* full geometry comparisons are only done for items that share 7146* at least one cell. As long as the geometries are relatively 7147* uniformly distributed across the plane, this greatly reduces 7148* the number of comparisons necessary. 7149*/ 7150var GridIndex = class { 7151 constructor(width, height, cellSize) { 7152 const boxCells = this.boxCells = []; 7153 const circleCells = this.circleCells = []; 7154 this.xCellCount = Math.ceil(width / cellSize); 7155 this.yCellCount = Math.ceil(height / cellSize); 7156 for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { 7157 boxCells.push([]); 7158 circleCells.push([]); 7159 } 7160 this.circleKeys = []; 7161 this.boxKeys = []; 7162 this.bboxes = []; 7163 this.circles = []; 7164 this.width = width; 7165 this.height = height; 7166 this.xScale = this.xCellCount / width; 7167 this.yScale = this.yCellCount / height; 7168 this.boxUid = 0; 7169 this.circleUid = 0; 7170 } 7171 keysLength() { 7172 return this.boxKeys.length + this.circleKeys.length; 7173 } 7174 insert(key, x1, y1, x2, y2) { 7175 this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); 7176 this.boxKeys.push(key); 7177 this.bboxes.push(x1); 7178 this.bboxes.push(y1); 7179 this.bboxes.push(x2); 7180 this.bboxes.push(y2); 7181 } 7182 insertCircle(key, x, y, radius) { 7183 this._forEachCell(x - radius, y - radius, x + radius, y + radius, this._insertCircleCell, this.circleUid++); 7184 this.circleKeys.push(key); 7185 this.circles.push(x); 7186 this.circles.push(y); 7187 this.circles.push(radius); 7188 } 7189 _insertBoxCell(x1, y1, x2, y2, cellIndex, uid) { 7190 this.boxCells[cellIndex].push(uid); 7191 } 7192 _insertCircleCell(x1, y1, x2, y2, cellIndex, uid) { 7193 this.circleCells[cellIndex].push(uid); 7194 } 7195 _query(x1, y1, x2, y2, hitTest, overlapMode, predicate) { 7196 if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) return []; 7197 const result = []; 7198 if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { 7199 if (hitTest) return [{ 7200 key: null, 7201 x1, 7202 y1, 7203 x2, 7204 y2 7205 }]; 7206 for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) result.push({ 7207 key: this.boxKeys[boxUid], 7208 x1: this.bboxes[boxUid * 4], 7209 y1: this.bboxes[boxUid * 4 + 1], 7210 x2: this.bboxes[boxUid * 4 + 2], 7211 y2: this.bboxes[boxUid * 4 + 3] 7212 }); 7213 for (let circleUid = 0; circleUid < this.circleKeys.length; circleUid++) { 7214 const x = this.circles[circleUid * 3]; 7215 const y = this.circles[circleUid * 3 + 1]; 7216 const radius = this.circles[circleUid * 3 + 2]; 7217 result.push({ 7218 key: this.circleKeys[circleUid], 7219 x1: x - radius, 7220 y1: y - radius, 7221 x2: x + radius, 7222 y2: y + radius 7223 }); 7224 } 7225 } else { 7226 const queryArgs = { 7227 hitTest, 7228 overlapMode, 7229 seenUids: { 7230 box: {}, 7231 circle: {} 7232 } 7233 }; 7234 this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate); 7235 } 7236 return result; 7237 } 7238 query(x1, y1, x2, y2) { 7239 return this._query(x1, y1, x2, y2, false, null); 7240 } 7241 hitTest(x1, y1, x2, y2, overlapMode, predicate) { 7242 return this._query(x1, y1, x2, y2, true, overlapMode, predicate).length > 0; 7243 } 7244 hitTestCircle(x, y, radius, overlapMode, predicate) { 7245 const x1 = x - radius; 7246 const x2 = x + radius; 7247 const y1 = y - radius; 7248 const y2 = y + radius; 7249 if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) return false; 7250 const result = []; 7251 const queryArgs = { 7252 hitTest: true, 7253 overlapMode, 7254 circle: { 7255 x, 7256 y, 7257 radius 7258 }, 7259 seenUids: { 7260 box: {}, 7261 circle: {} 7262 } 7263 }; 7264 this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs, predicate); 7265 return result.length > 0; 7266 } 7267 _queryCell(x1, y1, x2, y2, cellIndex, result, queryArgs, predicate) { 7268 const { seenUids, hitTest, overlapMode } = queryArgs; 7269 const boxCell = this.boxCells[cellIndex]; 7270 const epsilon = 1e-6; 7271 if (boxCell !== null) { 7272 const bboxes = this.bboxes; 7273 for (const boxUid of boxCell) if (!seenUids.box[boxUid]) { 7274 seenUids.box[boxUid] = true; 7275 const offset = boxUid * 4; 7276 const key = this.boxKeys[boxUid]; 7277 if (x1 <= bboxes[offset + 2] + epsilon && y1 <= bboxes[offset + 3] + epsilon && x2 >= bboxes[offset + 0] - epsilon && y2 >= bboxes[offset + 1] - epsilon && (!predicate || predicate(key))) { 7278 if (!hitTest || !overlapAllowed(overlapMode, key.overlapMode)) { 7279 result.push({ 7280 key, 7281 x1: bboxes[offset], 7282 y1: bboxes[offset + 1], 7283 x2: bboxes[offset + 2], 7284 y2: bboxes[offset + 3] 7285 }); 7286 if (hitTest) return true; 7287 } 7288 } 7289 } 7290 } 7291 const circleCell = this.circleCells[cellIndex]; 7292 if (circleCell !== null) { 7293 const circles = this.circles; 7294 for (const circleUid of circleCell) if (!seenUids.circle[circleUid]) { 7295 seenUids.circle[circleUid] = true; 7296 const offset = circleUid * 3; 7297 const key = this.circleKeys[circleUid]; 7298 if (this._circleAndRectCollide(circles[offset], circles[offset + 1], circles[offset + 2], x1, y1, x2, y2) && (!predicate || predicate(key))) { 7299 if (!hitTest || !overlapAllowed(overlapMode, key.overlapMode)) { 7300 const x = circles[offset]; 7301 const y = circles[offset + 1]; 7302 const radius = circles[offset + 2]; 7303 result.push({ 7304 key, 7305 x1: x - radius, 7306 y1: y - radius, 7307 x2: x + radius, 7308 y2: y + radius 7309 }); 7310 if (hitTest) return true; 7311 } 7312 } 7313 } 7314 } 7315 return false; 7316 } 7317 _queryCellCircle(x1, y1, x2, y2, cellIndex, result, queryArgs, predicate) { 7318 const { circle, seenUids, overlapMode } = queryArgs; 7319 const boxCell = this.boxCells[cellIndex]; 7320 if (boxCell !== null) { 7321 const bboxes = this.bboxes; 7322 for (const boxUid of boxCell) if (!seenUids.box[boxUid]) { 7323 seenUids.box[boxUid] = true; 7324 const offset = boxUid * 4; 7325 const key = this.boxKeys[boxUid]; 7326 if (this._circleAndRectCollide(circle.x, circle.y, circle.radius, bboxes[offset + 0], bboxes[offset + 1], bboxes[offset + 2], bboxes[offset + 3]) && (!predicate || predicate(key)) && !overlapAllowed(overlapMode, key.overlapMode)) { 7327 result.push(true); 7328 return true; 7329 } 7330 } 7331 } 7332 const circleCell = this.circleCells[cellIndex]; 7333 if (circleCell !== null) { 7334 const circles = this.circles; 7335 for (const circleUid of circleCell) if (!seenUids.circle[circleUid]) { 7336 seenUids.circle[circleUid] = true; 7337 const offset = circleUid * 3; 7338 const key = this.circleKeys[circleUid]; 7339 if (this._circlesCollide(circles[offset], circles[offset + 1], circles[offset + 2], circle.x, circle.y, circle.radius) && (!predicate || predicate(key)) && !overlapAllowed(overlapMode, key.overlapMode)) { 7340 result.push(true); 7341 return true; 7342 } 7343 } 7344 } 7345 } 7346 _forEachCell(x1, y1, x2, y2, fn, arg1, arg2, predicate) { 7347 const cx1 = this._convertToXCellCoord(x1); 7348 const cy1 = this._convertToYCellCoord(y1); 7349 const cx2 = this._convertToXCellCoord(x2); 7350 const cy2 = this._convertToYCellCoord(y2); 7351 for (let x = cx1; x <= cx2; x++) for (let y = cy1; y <= cy2; y++) { 7352 const cellIndex = this.xCellCount * y + x; 7353 if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return; 7354 } 7355 } 7356 _convertToXCellCoord(x) { 7357 return Math.max(0, Math.min(this.xCellCount - 1, Math.floor(x * this.xScale))); 7358 } 7359 _convertToYCellCoord(y) { 7360 return Math.max(0, Math.min(this.yCellCount - 1, Math.floor(y * this.yScale))); 7361 } 7362 _circlesCollide(x1, y1, r1, x2, y2, r2) { 7363 const dx = x2 - x1; 7364 const dy = y2 - y1; 7365 const bothRadii = r1 + r2; 7366 return bothRadii * bothRadii > dx * dx + dy * dy; 7367 } 7368 _circleAndRectCollide(circleX, circleY, radius, x1, y1, x2, y2) { 7369 const halfRectWidth = (x2 - x1) / 2; 7370 const distX = Math.abs(circleX - (x1 + halfRectWidth)); 7371 if (distX > halfRectWidth + radius) return false; 7372 const halfRectHeight = (y2 - y1) / 2; 7373 const distY = Math.abs(circleY - (y1 + halfRectHeight)); 7374 if (distY > halfRectHeight + radius) return false; 7375 if (distX <= halfRectWidth || distY <= halfRectHeight) return true; 7376 const dx = distX - halfRectWidth; 7377 const dy = distY - halfRectHeight; 7378 return dx * dx + dy * dy <= radius * radius; 7379 } 7380}; 7381//#endregion 7382//#region src/util/fast_maths.ts 7383function fastInvertTransformMat4(dst, src) { 7384 const sXYSqInv = 1 / (src[0] * src[0] + src[1] * src[1] + src[2] * src[2]); 7385 const sZSqInv = 1 / (src[8] * src[8] + src[9] * src[9] + src[10] * src[10]); 7386 const s0 = src[0] * sXYSqInv; 7387 const s4 = src[4] * sXYSqInv; 7388 const s8 = src[8] * sZSqInv; 7389 const s1 = src[1] * sXYSqInv; 7390 const s5 = src[5] * sXYSqInv; 7391 const s9 = src[9] * sZSqInv; 7392 const s2 = src[2] * sXYSqInv; 7393 const s6 = src[6] * sXYSqInv; 7394 const s10 = src[10] * sZSqInv; 7395 dst[0] = s0; 7396 dst[1] = s4; 7397 dst[2] = s8; 7398 dst[4] = s1; 7399 dst[5] = s5; 7400 dst[6] = s9; 7401 dst[8] = s2; 7402 dst[9] = s6; 7403 dst[10] = s10; 7404 const t0 = src[12]; 7405 const t1 = src[13]; 7406 const t2 = src[14]; 7407 dst[12] = -s0 * t0 - s1 * t1 - s2 * t2; 7408 dst[13] = -s4 * t0 - s5 * t1 - s6 * t2; 7409 dst[14] = -s8 * t0 - s9 * t1 - s10 * t2; 7410 dst[3] = 0; 7411 dst[7] = 0; 7412 dst[11] = 0; 7413 dst[15] = 1; 7414 return dst; 7415} 7416function fastInvertProjMat4(dst, src) { 7417 dst[0] = 1 / src[0]; 7418 dst[1] = 0; 7419 dst[2] = 0; 7420 dst[3] = 0; 7421 dst[4] = 0; 7422 dst[5] = 1 / src[5]; 7423 dst[6] = 0; 7424 dst[7] = 0; 7425 dst[8] = 0; 7426 dst[9] = 0; 7427 dst[10] = 0; 7428 dst[11] = 1 / src[14]; 7429 dst[12] = 0; 7430 dst[13] = 0; 7431 dst[14] = -1; 7432 dst[15] = src[10] / src[14]; 7433 return dst; 7434} 7435function fastInvertSkewMat4(dst, src) { 7436 const invDetXY = 1 / (src[0] * src[5] - src[1] * src[4]); 7437 dst[0] = src[5] * invDetXY; 7438 dst[1] = -src[1] * invDetXY; 7439 dst[2] = 0; 7440 dst[3] = 0; 7441 dst[4] = -src[4] * invDetXY; 7442 dst[5] = src[0] * invDetXY; 7443 dst[6] = 0; 7444 dst[7] = 0; 7445 dst[8] = 0; 7446 dst[9] = 0; 7447 dst[10] = 1 / src[10]; 7448 dst[11] = 0; 7449 dst[12] = 0; 7450 dst[13] = 0; 7451 dst[14] = 0; 7452 dst[15] = 1 / src[15]; 7453 return dst; 7454} 7455//#endregion 7456//#region src/symbol/projection.ts 7457/** 7458* Pre-allocate objects to avoid online allocation 7459*/ 7460const tmpMat4 = create$1(); 7461function getPitchedLabelPlaneMatrix(rotateWithMap, transform, pixelsToTileUnits) { 7462 const m = create$1(); 7463 if (!rotateWithMap) { 7464 const { vecSouth, vecEast } = getTileSkewVectors(transform); 7465 const skew = create$2(); 7466 skew[0] = vecEast[0]; 7467 skew[1] = vecEast[1]; 7468 skew[2] = vecSouth[0]; 7469 skew[3] = vecSouth[1]; 7470 invert(skew, skew); 7471 m[0] = skew[0]; 7472 m[1] = skew[1]; 7473 m[4] = skew[2]; 7474 m[5] = skew[3]; 7475 } 7476 scale(m, m, [ 7477 1 / pixelsToTileUnits, 7478 1 / pixelsToTileUnits, 7479 1 7480 ]); 7481 return m; 7482} 7483function getGlCoordMatrix(pitchWithMap, rotateWithMap, transform, pixelsToTileUnits) { 7484 if (pitchWithMap) { 7485 const m = create$1(); 7486 if (!rotateWithMap) { 7487 const { vecSouth, vecEast } = getTileSkewVectors(transform); 7488 m[0] = vecEast[0]; 7489 m[1] = vecEast[1]; 7490 m[4] = vecSouth[0]; 7491 m[5] = vecSouth[1]; 7492 } 7493 scale(m, m, [ 7494 pixelsToTileUnits, 7495 pixelsToTileUnits, 7496 1 7497 ]); 7498 return m; 7499 } else return transform.pixelsToClipSpaceMatrix; 7500} 7501function getTileSkewVectors(transform) { 7502 const cosRoll = Math.cos(transform.rollInRadians); 7503 const sinRoll = Math.sin(transform.rollInRadians); 7504 const cosPitch = Math.cos(transform.pitchInRadians); 7505 const cosBearing = Math.cos(transform.bearingInRadians); 7506 const sinBearing = Math.sin(transform.bearingInRadians); 7507 const vecSouth = create$3(); 7508 vecSouth[0] = -cosBearing * cosPitch * sinRoll - sinBearing * cosRoll; 7509 vecSouth[1] = -sinBearing * cosPitch * sinRoll + cosBearing * cosRoll; 7510 const vecSouthLen = length$1(vecSouth); 7511 if (vecSouthLen < 1e-9) zero(vecSouth); 7512 else scale$3(vecSouth, vecSouth, 1 / vecSouthLen); 7513 const vecEast = create$3(); 7514 vecEast[0] = cosBearing * cosPitch * cosRoll - sinBearing * sinRoll; 7515 vecEast[1] = sinBearing * cosPitch * cosRoll + cosBearing * sinRoll; 7516 const vecEastLen = length$1(vecEast); 7517 if (vecEastLen < 1e-9) zero(vecEast); 7518 else scale$3(vecEast, vecEast, 1 / vecEastLen); 7519 return { 7520 vecEast, 7521 vecSouth 7522 }; 7523} 7524/** 7525* Resolves the elevation of the symbol described by `projectionContext` at a tile coordinate. 7526*/ 7527function elevationAt(projectionContext, x, y) { 7528 return getSymbolElevation(projectionContext.getElevation, x, y, projectionContext.heightOffset ?? 0, projectionContext.heightAnchorGround ?? true); 7529} 7530/** 7531* Projects a point using a specified matrix, including the perspective divide. 7532* Uses a fast path if `elevation` is undefined. 7533*/ 7534function projectWithMatrix(x, y, matrix, elevation) { 7535 let pos; 7536 if (elevation != null) { 7537 pos = [ 7538 x, 7539 y, 7540 elevation, 7541 1 7542 ]; 7543 transformMat4(pos, pos, matrix); 7544 } else { 7545 pos = [ 7546 x, 7547 y, 7548 0, 7549 1 7550 ]; 7551 xyTransformMat4(pos, pos, matrix); 7552 } 7553 const w = pos[3]; 7554 return { 7555 point: new Point(pos[0] / w, pos[1] / w), 7556 signedDistanceFromCamera: w, 7557 isOccluded: false 7558 }; 7559} 7560function getPerspectiveRatio(cameraToCenterDistance, signedDistanceFromCamera) { 7561 return .5 + .5 * (cameraToCenterDistance / signedDistanceFromCamera); 7562} 7563function isVisible(p, clippingBuffer) { 7564 return p.x >= -clippingBuffer[0] && p.x <= clippingBuffer[0] && p.y >= -clippingBuffer[1] && p.y <= clippingBuffer[1]; 7565} 7566function updateLineLabels(bucket, painter, isText, pitchedLabelPlaneMatrix, pitchedLabelPlaneMatrixInverse, pitchWithMap, keepUpright, rotateToLine, unwrappedTileID, viewportWidth, viewportHeight, translation, getElevation) { 7567 const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; 7568 const partiallyEvaluatedSize = evaluateSizeForZoom(sizeData, painter.transform.zoom); 7569 const clippingBuffer = [256 / painter.width * 2 + 1, 256 / painter.height * 2 + 1]; 7570 const dynamicLayoutVertexArray = isText ? bucket.text.dynamicLayoutVertexArray : bucket.icon.dynamicLayoutVertexArray; 7571 dynamicLayoutVertexArray.clear(); 7572 const lineVertexArray = bucket.lineVertexArray; 7573 const placedSymbols = isText ? bucket.text.placedSymbolArray : bucket.icon.placedSymbolArray; 7574 const aspectRatio = painter.transform.width / painter.transform.height; 7575 let useVertical = false; 7576 for (let s = 0; s < placedSymbols.length; s++) { 7577 const symbol = placedSymbols.get(s); 7578 if (symbol.hidden || symbol.writingMode === 2 && !useVertical) { 7579 hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); 7580 continue; 7581 } 7582 useVertical = false; 7583 const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); 7584 const projectionContext = { 7585 getElevation, 7586 pitchedLabelPlaneMatrix, 7587 lineVertexArray, 7588 pitchWithMap, 7589 projectionCache: { 7590 projections: {}, 7591 offsets: {}, 7592 cachedAnchorPoint: void 0, 7593 anyProjectionOccluded: false 7594 }, 7595 transform: painter.transform, 7596 tileAnchorPoint, 7597 unwrappedTileID, 7598 width: viewportWidth, 7599 height: viewportHeight, 7600 translation 7601 }; 7602 const anchorPos = projectTileCoordinatesToClipSpace(symbol.anchorX, symbol.anchorY, projectionContext); 7603 if (!isVisible(anchorPos.point, clippingBuffer)) { 7604 hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); 7605 continue; 7606 } 7607 const cameraToAnchorDistance = anchorPos.signedDistanceFromCamera; 7608 const perspectiveRatio = getPerspectiveRatio(painter.transform.cameraToCenterDistance, cameraToAnchorDistance); 7609 const fontSize = evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol); 7610 const pitchScaledFontSize = pitchWithMap ? fontSize * painter.transform.getPitchedTextCorrection(symbol.anchorX, symbol.anchorY, unwrappedTileID) / perspectiveRatio : fontSize * perspectiveRatio; 7611 const placeUnflipped = placeGlyphsAlongLine({ 7612 projectionContext, 7613 pitchedLabelPlaneMatrixInverse, 7614 symbol, 7615 fontSize: pitchScaledFontSize, 7616 flip: false, 7617 keepUpright, 7618 glyphOffsetArray: bucket.glyphOffsetArray, 7619 dynamicLayoutVertexArray, 7620 aspectRatio, 7621 rotateToLine 7622 }); 7623 useVertical = placeUnflipped.useVertical; 7624 if (placeUnflipped.notEnoughRoom || useVertical || placeUnflipped.needsFlipping && placeGlyphsAlongLine({ 7625 projectionContext, 7626 pitchedLabelPlaneMatrixInverse, 7627 symbol, 7628 fontSize: pitchScaledFontSize, 7629 flip: true, 7630 keepUpright, 7631 glyphOffsetArray: bucket.glyphOffsetArray, 7632 dynamicLayoutVertexArray, 7633 aspectRatio, 7634 rotateToLine 7635 }).notEnoughRoom) hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); 7636 } 7637 if (isText) bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray); 7638 else bucket.icon.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray); 7639} 7640function placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, symbol, rotateToLine, projectionContext) { 7641 const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; 7642 const lineStartIndex = symbol.lineStartIndex; 7643 const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; 7644 const firstGlyphOffset = glyphOffsetArray.getoffsetX(symbol.glyphStartIndex); 7645 const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1); 7646 const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, symbol.segment, lineStartIndex, lineEndIndex, projectionContext, rotateToLine); 7647 if (!firstPlacedGlyph) return null; 7648 const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, symbol.segment, lineStartIndex, lineEndIndex, projectionContext, rotateToLine); 7649 if (!lastPlacedGlyph) return null; 7650 if (projectionContext.projectionCache.anyProjectionOccluded) return null; 7651 return { 7652 first: firstPlacedGlyph, 7653 last: lastPlacedGlyph 7654 }; 7655} 7656function requiresOrientationChange(writingMode, firstPoint, lastPoint, aspectRatio) { 7657 if (writingMode === 1) { 7658 if (Math.abs(lastPoint.y - firstPoint.y) > Math.abs(lastPoint.x - firstPoint.x) * aspectRatio) return { useVertical: true }; 7659 } 7660 if (writingMode === 2 ? firstPoint.y < lastPoint.y : firstPoint.x > lastPoint.x) return { needsFlipping: true }; 7661 return null; 7662} 7663function placeGlyphsAlongLine(args) { 7664 const { projectionContext, pitchedLabelPlaneMatrixInverse, symbol, fontSize, flip, keepUpright, glyphOffsetArray, dynamicLayoutVertexArray, aspectRatio, rotateToLine } = args; 7665 const fontScale = fontSize / 24; 7666 const lineOffsetX = symbol.lineOffsetX * fontScale; 7667 const lineOffsetY = symbol.lineOffsetY * fontScale; 7668 let placedGlyphs; 7669 if (symbol.numGlyphs > 1) { 7670 const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; 7671 const lineStartIndex = symbol.lineStartIndex; 7672 const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; 7673 const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, symbol, rotateToLine, projectionContext); 7674 if (!firstAndLastGlyph) return { notEnoughRoom: true }; 7675 const firstPoint = projectFromLabelPlaneToClipSpace(firstAndLastGlyph.first.point.x, firstAndLastGlyph.first.point.y, projectionContext, pitchedLabelPlaneMatrixInverse); 7676 const lastPoint = projectFromLabelPlaneToClipSpace(firstAndLastGlyph.last.point.x, firstAndLastGlyph.last.point.y, projectionContext, pitchedLabelPlaneMatrixInverse); 7677 if (keepUpright && !flip) { 7678 const orientationChange = requiresOrientationChange(symbol.writingMode, firstPoint, lastPoint, aspectRatio); 7679 if (orientationChange) return orientationChange; 7680 } 7681 placedGlyphs = [firstAndLastGlyph.first]; 7682 for (let glyphIndex = symbol.glyphStartIndex + 1; glyphIndex < glyphEndIndex - 1; glyphIndex++) { 7683 const placedGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, symbol.segment, lineStartIndex, lineEndIndex, projectionContext, rotateToLine); 7684 if (!placedGlyph) return { notEnoughRoom: true }; 7685 placedGlyphs.push(placedGlyph); 7686 } 7687 placedGlyphs.push(firstAndLastGlyph.last); 7688 } else { 7689 if (keepUpright && !flip) { 7690 const a = projectTileCoordinatesToLabelPlane(projectionContext.tileAnchorPoint.x, projectionContext.tileAnchorPoint.y, projectionContext).point; 7691 const tileVertexIndex = symbol.lineStartIndex + symbol.segment + 1; 7692 const tileSegmentEnd = new Point(projectionContext.lineVertexArray.getx(tileVertexIndex), projectionContext.lineVertexArray.gety(tileVertexIndex)); 7693 const projectedVertex = projectTileCoordinatesToLabelPlane(tileSegmentEnd.x, tileSegmentEnd.y, projectionContext); 7694 const b = projectedVertex.signedDistanceFromCamera > 0 ? projectedVertex.point : projectTruncatedLineSegmentToLabelPlane(projectionContext.tileAnchorPoint, tileSegmentEnd, a, 1, projectionContext); 7695 const clipSpaceA = projectFromLabelPlaneToClipSpace(a.x, a.y, projectionContext, pitchedLabelPlaneMatrixInverse); 7696 const clipSpaceB = projectFromLabelPlaneToClipSpace(b.x, b.y, projectionContext, pitchedLabelPlaneMatrixInverse); 7697 const orientationChange = requiresOrientationChange(symbol.writingMode, clipSpaceA, clipSpaceB, aspectRatio); 7698 if (orientationChange) return orientationChange; 7699 } 7700 const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, symbol.segment, symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, projectionContext, rotateToLine); 7701 if (!singleGlyph || projectionContext.projectionCache.anyProjectionOccluded) return { notEnoughRoom: true }; 7702 placedGlyphs = [singleGlyph]; 7703 } 7704 for (const glyph of placedGlyphs) addDynamicAttributes(dynamicLayoutVertexArray, glyph.point, glyph.angle); 7705 return {}; 7706} 7707/** 7708* Takes a line and direction from `previousTilePoint` to `currentTilePoint`, projects it to the correct label plane, 7709* and returns a projected point along this projected line that is `minimumLength` distance away from `previousProjectedPoint`. 7710* Projects a "virtual" vertex along a line segment. 7711* @param previousTilePoint - Line start point, in tile coordinates. 7712* @param currentTilePoint - Line end point, in tile coordinates. 7713* @param previousProjectedPoint - Projection of `previousTilePoint` into label plane 7714* @param minimumLength - Distance in the projected space along the line for the returned point. 7715* @param projectionContext - Projection context, used to get terrain's `getElevation`, and to project the points to screen pixels. 7716*/ 7717function projectTruncatedLineSegmentToLabelPlane(previousTilePoint, currentTilePoint, previousProjectedPoint, minimumLength, projectionContext) { 7718 const unitVertexToBeProjected = previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()); 7719 const projectedUnitVertex = projectTileCoordinatesToLabelPlane(unitVertexToBeProjected.x, unitVertexToBeProjected.y, projectionContext).point; 7720 const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex); 7721 return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag())); 7722} 7723/** 7724* Transform a vertex from tile coordinates to label plane coordinates 7725* @param index - index of vertex to project 7726* @param projectionContext - necessary data to project a vertex 7727* @returns the vertex projected to the label plane 7728*/ 7729function projectLineVertexToLabelPlane(index, projectionContext, syntheticVertexArgs) { 7730 const cache = projectionContext.projectionCache; 7731 if (cache.projections[index]) return cache.projections[index]; 7732 const currentVertex = new Point(projectionContext.lineVertexArray.getx(index), projectionContext.lineVertexArray.gety(index)); 7733 const projection = projectTileCoordinatesToLabelPlane(currentVertex.x, currentVertex.y, projectionContext); 7734 if (projection.signedDistanceFromCamera > 0) { 7735 cache.projections[index] = projection.point; 7736 cache.anyProjectionOccluded ||= projection.isOccluded; 7737 return projection.point; 7738 } 7739 const previousLineVertexIndex = index - syntheticVertexArgs.direction; 7740 const previousTilePoint = syntheticVertexArgs.distanceFromAnchor === 0 ? projectionContext.tileAnchorPoint : new Point(projectionContext.lineVertexArray.getx(previousLineVertexIndex), projectionContext.lineVertexArray.gety(previousLineVertexIndex)); 7741 const minimumLength = syntheticVertexArgs.absOffsetX - syntheticVertexArgs.distanceFromAnchor + 1; 7742 return projectTruncatedLineSegmentToLabelPlane(previousTilePoint, currentVertex, syntheticVertexArgs.previousVertex, minimumLength, projectionContext); 7743} 7744/** 7745* Projects the given point in tile coordinates to the correct label plane. 7746* If pitchWithMap is true, the (rotated) map plane in pixels is used, 7747* otherwise screen pixels are used. 7748*/ 7749function projectTileCoordinatesToLabelPlane(x, y, projectionContext) { 7750 const translatedX = x + projectionContext.translation[0]; 7751 const translatedY = y + projectionContext.translation[1]; 7752 let projection; 7753 if (projectionContext.pitchWithMap) { 7754 projection = projectWithMatrix(translatedX, translatedY, projectionContext.pitchedLabelPlaneMatrix, elevationAt(projectionContext, translatedX, translatedY)); 7755 projection.isOccluded = false; 7756 } else { 7757 projection = projectionContext.transform.projectTileCoordinates(translatedX, translatedY, projectionContext.unwrappedTileID, elevationAt(projectionContext, translatedX, translatedY)); 7758 projection.point.x = (projection.point.x * .5 + .5) * projectionContext.width; 7759 projection.point.y = (-projection.point.y * .5 + .5) * projectionContext.height; 7760 } 7761 return projection; 7762} 7763function projectFromLabelPlaneToClipSpace(x, y, projectionContext, pitchedLabelPlaneMatrixInverse) { 7764 if (projectionContext.pitchWithMap) { 7765 const pos = [ 7766 x, 7767 y, 7768 0, 7769 1 7770 ]; 7771 transformMat4(pos, pos, pitchedLabelPlaneMatrixInverse); 7772 const tileX = pos[0] / pos[3]; 7773 const tileY = pos[1] / pos[3]; 7774 return projectionContext.transform.projectTileCoordinates(tileX, tileY, projectionContext.unwrappedTileID, elevationAt(projectionContext, tileX, tileY)).point; 7775 } else return { 7776 x: x / projectionContext.width * 2 - 1, 7777 y: 1 - y / projectionContext.height * 2 7778 }; 7779} 7780/** 7781* Projects the given point in tile coordinates to the GL clip space (-1..1). 7782*/ 7783function projectTileCoordinatesToClipSpace(x, y, projectionContext) { 7784 return projectionContext.transform.projectTileCoordinates(x, y, projectionContext.unwrappedTileID, elevationAt(projectionContext, x, y)); 7785} 7786/** 7787* Calculate the normal vector for a line segment 7788* @param segmentVector - will be mutated as a tiny optimization 7789* @param offset - magnitude of resulting vector 7790* @param direction - direction of line traversal 7791* @returns a normal vector from the segment, with magnitude equal to offset amount 7792*/ 7793function transformToOffsetNormal(segmentVector, offset, direction) { 7794 return segmentVector._unit()._perp()._mult(offset * direction); 7795} 7796/** 7797* Construct offset line segments for the current segment and the next segment, then extend/shrink 7798* the segments until they intersect. If the segments are parallel, then they will touch with no modification. 7799* 7800* @param index - Index of the current vertex 7801* @param prevToCurrentOffsetNormal - Normal vector of the line segment from the previous vertex to the current vertex 7802* @param currentVertex - Current (non-offset) vertex projected to the label plane 7803* @param lineStartIndex - Beginning index for the line this label is on 7804* @param lineEndIndex - End index for the line this label is on 7805* @param offsetPreviousVertex - The previous vertex projected to the label plane, and then offset along the previous segments normal 7806* @param lineOffsetY - Magnitude of the offset 7807* @param projectionContext - Necessary data for tile-to-label-plane projection 7808* @returns The point at which the current and next line segments intersect, once offset and extended/shrunk to their meeting point 7809*/ 7810function findOffsetIntersectionPoint(index, prevToCurrentOffsetNormal, currentVertex, lineStartIndex, lineEndIndex, offsetPreviousVertex, lineOffsetY, projectionContext, syntheticVertexArgs) { 7811 if (projectionContext.projectionCache.offsets[index]) return projectionContext.projectionCache.offsets[index]; 7812 const offsetCurrentVertex = currentVertex.add(prevToCurrentOffsetNormal); 7813 if (index + syntheticVertexArgs.direction < lineStartIndex || index + syntheticVertexArgs.direction >= lineEndIndex) { 7814 projectionContext.projectionCache.offsets[index] = offsetCurrentVertex; 7815 return offsetCurrentVertex; 7816 } 7817 const nextVertex = projectLineVertexToLabelPlane(index + syntheticVertexArgs.direction, projectionContext, syntheticVertexArgs); 7818 const currentToNextOffsetNormal = transformToOffsetNormal(nextVertex.sub(currentVertex), lineOffsetY, syntheticVertexArgs.direction); 7819 const offsetNextSegmentBegin = currentVertex.add(currentToNextOffsetNormal); 7820 const offsetNextSegmentEnd = nextVertex.add(currentToNextOffsetNormal); 7821 projectionContext.projectionCache.offsets[index] = findLineIntersection(offsetPreviousVertex, offsetCurrentVertex, offsetNextSegmentBegin, offsetNextSegmentEnd) || offsetCurrentVertex; 7822 return projectionContext.projectionCache.offsets[index]; 7823} 7824function placeGlyphAlongLine(offsetX, lineOffsetX, lineOffsetY, flip, anchorSegment, lineStartIndex, lineEndIndex, projectionContext, rotateToLine) { 7825 const combinedOffsetX = flip ? offsetX - lineOffsetX : offsetX + lineOffsetX; 7826 let direction = combinedOffsetX > 0 ? 1 : -1; 7827 let angle = 0; 7828 if (flip) { 7829 direction *= -1; 7830 angle = Math.PI; 7831 } 7832 if (direction < 0) angle += Math.PI; 7833 let currentIndex = direction > 0 ? lineStartIndex + anchorSegment : lineStartIndex + anchorSegment + 1; 7834 let anchorPoint; 7835 if (projectionContext.projectionCache.cachedAnchorPoint) anchorPoint = projectionContext.projectionCache.cachedAnchorPoint; 7836 else { 7837 anchorPoint = projectTileCoordinatesToLabelPlane(projectionContext.tileAnchorPoint.x, projectionContext.tileAnchorPoint.y, projectionContext).point; 7838 projectionContext.projectionCache.cachedAnchorPoint = anchorPoint; 7839 } 7840 let currentVertex = anchorPoint; 7841 let previousVertex = anchorPoint; 7842 let offsetIntersectionPoint; 7843 let offsetPreviousVertex; 7844 let distanceFromAnchor = 0; 7845 let currentSegmentDistance = 0; 7846 const absOffsetX = Math.abs(combinedOffsetX); 7847 const pathVertices = []; 7848 let currentLineSegment; 7849 while (distanceFromAnchor + currentSegmentDistance <= absOffsetX) { 7850 currentIndex += direction; 7851 if (currentIndex < lineStartIndex || currentIndex >= lineEndIndex) return null; 7852 distanceFromAnchor += currentSegmentDistance; 7853 previousVertex = currentVertex; 7854 offsetPreviousVertex = offsetIntersectionPoint; 7855 const syntheticVertexArgs = { 7856 absOffsetX, 7857 direction, 7858 distanceFromAnchor, 7859 previousVertex 7860 }; 7861 currentVertex = projectLineVertexToLabelPlane(currentIndex, projectionContext, syntheticVertexArgs); 7862 if (lineOffsetY === 0) { 7863 pathVertices.push(previousVertex); 7864 currentLineSegment = currentVertex.sub(previousVertex); 7865 } else { 7866 let prevToCurrentOffsetNormal; 7867 const prevToCurrent = currentVertex.sub(previousVertex); 7868 if (prevToCurrent.mag() === 0) prevToCurrentOffsetNormal = transformToOffsetNormal(projectLineVertexToLabelPlane(currentIndex + direction, projectionContext, syntheticVertexArgs).sub(currentVertex), lineOffsetY, direction); 7869 else prevToCurrentOffsetNormal = transformToOffsetNormal(prevToCurrent, lineOffsetY, direction); 7870 offsetPreviousVertex ||= previousVertex.add(prevToCurrentOffsetNormal); 7871 offsetIntersectionPoint = findOffsetIntersectionPoint(currentIndex, prevToCurrentOffsetNormal, currentVertex, lineStartIndex, lineEndIndex, offsetPreviousVertex, lineOffsetY, projectionContext, syntheticVertexArgs); 7872 pathVertices.push(offsetPreviousVertex); 7873 currentLineSegment = offsetIntersectionPoint.sub(offsetPreviousVertex); 7874 } 7875 currentSegmentDistance = currentLineSegment.mag(); 7876 } 7877 const segmentInterpolationT = (absOffsetX - distanceFromAnchor) / currentSegmentDistance; 7878 const p = currentLineSegment._mult(segmentInterpolationT)._add(offsetPreviousVertex || previousVertex); 7879 const segmentAngle = angle + Math.atan2(currentVertex.y - previousVertex.y, currentVertex.x - previousVertex.x); 7880 pathVertices.push(p); 7881 return { 7882 point: p, 7883 angle: rotateToLine ? segmentAngle : 0, 7884 path: pathVertices 7885 }; 7886} 7887const hiddenGlyphAttributes = new Float32Array([ 7888 -Infinity, 7889 -Infinity, 7890 0, 7891 -Infinity, 7892 -Infinity, 7893 0, 7894 -Infinity, 7895 -Infinity, 7896 0, 7897 -Infinity, 7898 -Infinity, 7899 0 7900]); 7901function hideGlyphs(num, dynamicLayoutVertexArray) { 7902 for (let i = 0; i < num; i++) { 7903 const offset = dynamicLayoutVertexArray.length; 7904 dynamicLayoutVertexArray.resize(offset + 4); 7905 dynamicLayoutVertexArray.float32.set(hiddenGlyphAttributes, offset * 3); 7906 } 7907} 7908function xyTransformMat4(out, a, m) { 7909 const x = a[0], y = a[1]; 7910 out[0] = m[0] * x + m[4] * y + m[12]; 7911 out[1] = m[1] * x + m[5] * y + m[13]; 7912 out[3] = m[3] * x + m[7] * y + m[15]; 7913 return out; 7914} 7915/** 7916* Takes a path of points that was previously projected using the `pitchedLabelPlaneMatrix` 7917* and projects it using the map projection's (mercator/globe...) `projectTileCoordinates` function. 7918* Returns a new array of the projected points. 7919* Does not modify the input array. 7920*/ 7921function projectPathSpecialProjection(projectedPath, projectionContext) { 7922 const inverseLabelPlaneMatrix = tmpMat4; 7923 fastInvertSkewMat4(inverseLabelPlaneMatrix, projectionContext.pitchedLabelPlaneMatrix); 7924 return projectedPath.map((p) => { 7925 const backProjected = projectWithMatrix(p.x, p.y, inverseLabelPlaneMatrix, elevationAt(projectionContext, p.x, p.y)); 7926 const projected = projectionContext.transform.projectTileCoordinates(backProjected.point.x, backProjected.point.y, projectionContext.unwrappedTileID, elevationAt(projectionContext, backProjected.point.x, backProjected.point.y)); 7927 projected.point.x = (projected.point.x * .5 + .5) * projectionContext.width; 7928 projected.point.y = (-projected.point.y * .5 + .5) * projectionContext.height; 7929 return projected; 7930 }); 7931} 7932/** 7933* Takes a path of points projected to screenspace, finds the longest continuous unoccluded segment of that path 7934* and returns it. 7935* Does not modify the input array. 7936*/ 7937function pathSlicedToLongestUnoccluded(path) { 7938 let longestUnoccludedStart = 0; 7939 let longestUnoccludedLength = 0; 7940 let currentUnoccludedStart = 0; 7941 let currentUnoccludedLength = 0; 7942 for (let i = 0; i < path.length; i++) if (path[i].isOccluded) { 7943 currentUnoccludedStart = i + 1; 7944 currentUnoccludedLength = 0; 7945 } else { 7946 currentUnoccludedLength++; 7947 if (currentUnoccludedLength > longestUnoccludedLength) { 7948 longestUnoccludedLength = currentUnoccludedLength; 7949 longestUnoccludedStart = currentUnoccludedStart; 7950 } 7951 } 7952 return path.slice(longestUnoccludedStart, longestUnoccludedStart + longestUnoccludedLength); 7953} 7954/** 7955* @internal 7956* A collision index used to prevent symbols from overlapping. It keep tracks of 7957* where previous symbols have been placed and is used to check if a new 7958* symbol overlaps with any previously added symbols. 7959* 7960* There are two steps to insertion: first placeCollisionBox/Circles checks if 7961* there's room for a symbol, then insertCollisionBox/Circles actually puts the 7962* symbol in the index. The two step process allows paired symbols to be inserted 7963* together even if they overlap. 7964*/ 7965var CollisionIndex = class { 7966 constructor(transform, grid = new GridIndex(transform.width + 200, transform.height + 200, 25), ignoredGrid = new GridIndex(transform.width + 200, transform.height + 200, 25)) { 7967 this.transform = transform; 7968 this.grid = grid; 7969 this.ignoredGrid = ignoredGrid; 7970 this.pitchFactor = Math.cos(transform.pitch * Math.PI / 180) * transform.cameraToCenterDistance; 7971 this.screenRightBoundary = transform.width + 100; 7972 this.screenBottomBoundary = transform.height + 100; 7973 this.gridRightBoundary = transform.width + 200; 7974 this.gridBottomBoundary = transform.height + 200; 7975 this.perspectiveRatioCutoff = .6; 7976 } 7977 placeCollisionBox(collisionBox, overlapMode, textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translation, collisionGroupPredicate, getElevation, shift, simpleProjectionMatrix, heightOffset = 0, heightAnchorGround = true) { 7978 const x = collisionBox.anchorPointX + translation[0]; 7979 const y = collisionBox.anchorPointY + translation[1]; 7980 const projectedPoint = this.projectAndGetPerspectiveRatio(x, y, unwrappedTileID, getSymbolElevation(getElevation, x, y, heightOffset, heightAnchorGround), simpleProjectionMatrix); 7981 const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio; 7982 let projectedBox; 7983 if (!pitchWithMap && !rotateWithMap) { 7984 const pointX = projectedPoint.x + (shift ? shift.x * tileToViewport : 0); 7985 const pointY = projectedPoint.y + (shift ? shift.y * tileToViewport : 0); 7986 projectedBox = { 7987 allPointsOccluded: false, 7988 box: [ 7989 pointX + collisionBox.x1 * tileToViewport, 7990 pointY + collisionBox.y1 * tileToViewport, 7991 pointX + collisionBox.x2 * tileToViewport, 7992 pointY + collisionBox.y2 * tileToViewport 7993 ] 7994 }; 7995 } else projectedBox = this._projectCollisionBox(collisionBox, tileToViewport, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translation, projectedPoint, getElevation, shift, simpleProjectionMatrix, heightOffset, heightAnchorGround); 7996 const [tlX, tlY, brX, brY] = projectedBox.box; 7997 const occluded = pitchWithMap ? projectedBox.allPointsOccluded : projectedPoint.isOccluded; 7998 let unplaceable = occluded; 7999 unplaceable ||= projectedPoint.perspectiveRatio < this.perspectiveRatioCutoff; 8000 unplaceable ||= !this.isInsideGrid(tlX, tlY, brX, brY); 8001 if (unplaceable || overlapMode !== "always" && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate)) return { 8002 box: [ 8003 tlX, 8004 tlY, 8005 brX, 8006 brY 8007 ], 8008 placeable: false, 8009 offscreen: false, 8010 occluded 8011 }; 8012 return { 8013 box: [ 8014 tlX, 8015 tlY, 8016 brX, 8017 brY 8018 ], 8019 placeable: true, 8020 offscreen: this.isOffscreen(tlX, tlY, brX, brY), 8021 occluded 8022 }; 8023 } 8024 placeCollisionCircles(overlapMode, symbol, lineVertexArray, glyphOffsetArray, fontSize, unwrappedTileID, pitchedLabelPlaneMatrix, showCollisionCircles, pitchWithMap, collisionGroupPredicate, circlePixelDiameter, textPixelPadding, translation, getElevation) { 8025 const placedCollisionCircles = []; 8026 const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); 8027 const perspectiveRatio = this.getPerspectiveRatio(tileUnitAnchorPoint.x, tileUnitAnchorPoint.y, unwrappedTileID, getElevation?.(tileUnitAnchorPoint.x, tileUnitAnchorPoint.y)); 8028 const labelPlaneFontScale = (pitchWithMap ? fontSize * this.transform.getPitchedTextCorrection(symbol.anchorX, symbol.anchorY, unwrappedTileID) / perspectiveRatio : fontSize * perspectiveRatio) / 24; 8029 const projectionCache = { 8030 projections: {}, 8031 offsets: {}, 8032 cachedAnchorPoint: void 0, 8033 anyProjectionOccluded: false 8034 }; 8035 const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale; 8036 const lineOffsetY = symbol.lineOffsetY * labelPlaneFontScale; 8037 const projectionContext = { 8038 getElevation, 8039 pitchedLabelPlaneMatrix, 8040 lineVertexArray, 8041 pitchWithMap, 8042 projectionCache, 8043 transform: this.transform, 8044 tileAnchorPoint: tileUnitAnchorPoint, 8045 unwrappedTileID, 8046 width: this.transform.width, 8047 height: this.transform.height, 8048 translation 8049 }; 8050 const firstAndLastGlyph = placeFirstAndLastGlyph(labelPlaneFontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, false, symbol, false, projectionContext); 8051 let collisionDetected = false; 8052 let inGrid = false; 8053 let entirelyOffscreen = true; 8054 if (firstAndLastGlyph) { 8055 const radius = circlePixelDiameter * .5 * perspectiveRatio + textPixelPadding; 8056 const screenPlaneMin = new Point(-100, -100); 8057 const screenPlaneMax = new Point(this.screenRightBoundary, this.screenBottomBoundary); 8058 const interpolator = new PathInterpolator(); 8059 const first = firstAndLastGlyph.first; 8060 const last = firstAndLastGlyph.last; 8061 let projectedPath = []; 8062 for (let i = first.path.length - 1; i >= 1; i--) projectedPath.push(first.path[i]); 8063 for (let i = 1; i < last.path.length; i++) projectedPath.push(last.path[i]); 8064 const circleDist = radius * 2.5; 8065 if (pitchWithMap) { 8066 const screenSpacePath = this.projectPathToScreenSpace(projectedPath, projectionContext); 8067 if (screenSpacePath.some((point) => point.signedDistanceFromCamera <= 0)) projectedPath = []; 8068 else projectedPath = screenSpacePath.map((p) => p.point); 8069 } 8070 let segments = []; 8071 if (projectedPath.length > 0) { 8072 const minPoint = projectedPath[0].clone(); 8073 const maxPoint = projectedPath[0].clone(); 8074 for (let i = 1; i < projectedPath.length; i++) { 8075 minPoint.x = Math.min(minPoint.x, projectedPath[i].x); 8076 minPoint.y = Math.min(minPoint.y, projectedPath[i].y); 8077 maxPoint.x = Math.max(maxPoint.x, projectedPath[i].x); 8078 maxPoint.y = Math.max(maxPoint.y, projectedPath[i].y); 8079 } 8080 if (minPoint.x >= screenPlaneMin.x && maxPoint.x <= screenPlaneMax.x && minPoint.y >= screenPlaneMin.y && maxPoint.y <= screenPlaneMax.y) segments = [projectedPath]; 8081 else if (maxPoint.x < screenPlaneMin.x || minPoint.x > screenPlaneMax.x || maxPoint.y < screenPlaneMin.y || minPoint.y > screenPlaneMax.y) segments = []; 8082 else segments = clipLine([projectedPath], screenPlaneMin.x, screenPlaneMin.y, screenPlaneMax.x, screenPlaneMax.y); 8083 } 8084 for (const seg of segments) { 8085 interpolator.reset(seg, radius * .25); 8086 let numCircles = 0; 8087 if (interpolator.length <= .5 * radius) numCircles = 1; 8088 else numCircles = Math.ceil(interpolator.paddedLength / circleDist) + 1; 8089 for (let i = 0; i < numCircles; i++) { 8090 const t = i / Math.max(numCircles - 1, 1); 8091 const circlePosition = interpolator.lerp(t); 8092 const centerX = circlePosition.x + 100; 8093 const centerY = circlePosition.y + 100; 8094 placedCollisionCircles.push(centerX, centerY, radius, 0); 8095 const x1 = centerX - radius; 8096 const y1 = centerY - radius; 8097 const x2 = centerX + radius; 8098 const y2 = centerY + radius; 8099 entirelyOffscreen &&= this.isOffscreen(x1, y1, x2, y2); 8100 inGrid ||= this.isInsideGrid(x1, y1, x2, y2); 8101 if (overlapMode !== "always" && this.grid.hitTestCircle(centerX, centerY, radius, overlapMode, collisionGroupPredicate)) { 8102 collisionDetected = true; 8103 if (!showCollisionCircles) return { 8104 circles: [], 8105 offscreen: false, 8106 collisionDetected 8107 }; 8108 } 8109 } 8110 } 8111 } 8112 return { 8113 circles: !showCollisionCircles && collisionDetected || !inGrid || perspectiveRatio < this.perspectiveRatioCutoff ? [] : placedCollisionCircles, 8114 offscreen: entirelyOffscreen, 8115 collisionDetected 8116 }; 8117 } 8118 projectPathToScreenSpace(projectedPath, projectionContext) { 8119 return pathSlicedToLongestUnoccluded(projectPathSpecialProjection(projectedPath, projectionContext)); 8120 } 8121 /** 8122 * Because the geometries in the CollisionIndex are an approximation of the shape of 8123 * symbols on the map, we use the CollisionIndex to look up the symbol part of 8124 * `queryRenderedFeatures`. 8125 */ 8126 queryRenderedSymbols(viewportQueryGeometry) { 8127 if (viewportQueryGeometry.length === 0 || this.grid.keysLength() === 0 && this.ignoredGrid.keysLength() === 0) return {}; 8128 const query = []; 8129 const bounds = new Bounds(); 8130 for (const point of viewportQueryGeometry) { 8131 const gridPoint = new Point(point.x + 100, point.y + 100); 8132 bounds.extend(gridPoint); 8133 query.push(gridPoint); 8134 } 8135 const { minX, minY, maxX, maxY } = bounds; 8136 const features = this.grid.query(minX, minY, maxX, maxY).concat(this.ignoredGrid.query(minX, minY, maxX, maxY)); 8137 const seenFeatures = {}; 8138 const result = {}; 8139 for (const feature of features) { 8140 const featureKey = feature.key; 8141 if (seenFeatures[featureKey.bucketInstanceId] === void 0) seenFeatures[featureKey.bucketInstanceId] = {}; 8142 if (seenFeatures[featureKey.bucketInstanceId][featureKey.featureIndex]) continue; 8143 const bbox = [ 8144 new Point(feature.x1, feature.y1), 8145 new Point(feature.x2, feature.y1), 8146 new Point(feature.x2, feature.y2), 8147 new Point(feature.x1, feature.y2) 8148 ]; 8149 if (!polygonIntersectsPolygon(query, bbox)) continue; 8150 seenFeatures[featureKey.bucketInstanceId][featureKey.featureIndex] = true; 8151 if (result[featureKey.bucketInstanceId] === void 0) result[featureKey.bucketInstanceId] = []; 8152 result[featureKey.bucketInstanceId].push(featureKey.featureIndex); 8153 } 8154 return result; 8155 } 8156 insertCollisionBox(collisionBox, overlapMode, ignorePlacement, bucketInstanceId, featureIndex, collisionGroupID) { 8157 const grid = ignorePlacement ? this.ignoredGrid : this.grid; 8158 const key = { 8159 bucketInstanceId, 8160 featureIndex, 8161 collisionGroupID, 8162 overlapMode 8163 }; 8164 grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]); 8165 } 8166 insertCollisionCircles(collisionCircles, overlapMode, ignorePlacement, bucketInstanceId, featureIndex, collisionGroupID) { 8167 const grid = ignorePlacement ? this.ignoredGrid : this.grid; 8168 const key = { 8169 bucketInstanceId, 8170 featureIndex, 8171 collisionGroupID, 8172 overlapMode 8173 }; 8174 for (let k = 0; k < collisionCircles.length; k += 4) grid.insertCircle(key, collisionCircles[k], collisionCircles[k + 1], collisionCircles[k + 2]); 8175 } 8176 projectAndGetPerspectiveRatio(x, y, unwrappedTileID, elevation, simpleProjectionMatrix) { 8177 if (simpleProjectionMatrix) { 8178 let pos; 8179 if (elevation != null) { 8180 pos = [ 8181 x, 8182 y, 8183 elevation, 8184 1 8185 ]; 8186 transformMat4(pos, pos, simpleProjectionMatrix); 8187 } else { 8188 pos = [ 8189 x, 8190 y, 8191 0, 8192 1 8193 ]; 8194 xyTransformMat4(pos, pos, simpleProjectionMatrix); 8195 } 8196 const w = pos[3]; 8197 return { 8198 x: (pos[0] / w + 1) / 2 * this.transform.width + 100, 8199 y: (-pos[1] / w + 1) / 2 * this.transform.height + 100, 8200 perspectiveRatio: .5 + .5 * (this.transform.cameraToCenterDistance / w), 8201 isOccluded: false, 8202 signedDistanceFromCamera: w 8203 }; 8204 } else { 8205 const projected = this.transform.projectTileCoordinates(x, y, unwrappedTileID, elevation); 8206 return { 8207 x: (projected.point.x + 1) / 2 * this.transform.width + 100, 8208 y: (-projected.point.y + 1) / 2 * this.transform.height + 100, 8209 perspectiveRatio: .5 + .5 * (this.transform.cameraToCenterDistance / projected.signedDistanceFromCamera), 8210 isOccluded: projected.isOccluded, 8211 signedDistanceFromCamera: projected.signedDistanceFromCamera 8212 }; 8213 } 8214 } 8215 getPerspectiveRatio(x, y, unwrappedTileID, elevation) { 8216 const projected = this.transform.projectTileCoordinates(x, y, unwrappedTileID, elevation); 8217 return .5 + .5 * (this.transform.cameraToCenterDistance / projected.signedDistanceFromCamera); 8218 } 8219 isOffscreen(x1, y1, x2, y2) { 8220 return x2 < 100 || x1 >= this.screenRightBoundary || y2 < 100 || y1 > this.screenBottomBoundary; 8221 } 8222 isInsideGrid(x1, y1, x2, y2) { 8223 return x2 >= 0 && x1 < this.gridRightBoundary && y2 >= 0 && y1 < this.gridBottomBoundary; 8224 } 8225 getViewportMatrix() { 8226 const m = identity([]); 8227 translate(m, m, [ 8228 -100, 8229 -100, 8230 0 8231 ]); 8232 return m; 8233 } 8234 /** 8235 * Applies all layout+paint properties of the given box in order to find as good approximation of its screen-space bounding box as possible. 8236 */ 8237 _projectCollisionBox(collisionBox, tileToViewport, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translation, projectedPoint, getElevation, shift, simpleProjectionMatrix, heightOffset = 0, heightAnchorGround = true) { 8238 let vecEastX = 1; 8239 let vecEastY = 0; 8240 let vecSouthX = 0; 8241 let vecSouthY = 1; 8242 const translatedAnchorX = collisionBox.anchorPointX + translation[0]; 8243 const translatedAnchorY = collisionBox.anchorPointY + translation[1]; 8244 if (rotateWithMap && !pitchWithMap) { 8245 const projectedEast = this.projectAndGetPerspectiveRatio(translatedAnchorX + 1, translatedAnchorY, unwrappedTileID, getSymbolElevation(getElevation, translatedAnchorX + 1, translatedAnchorY, heightOffset, heightAnchorGround), simpleProjectionMatrix); 8246 const toEastX = projectedEast.x - projectedPoint.x; 8247 const toEastY = projectedEast.y - projectedPoint.y; 8248 const angle = Math.atan(toEastY / toEastX) + (toEastX < 0 ? Math.PI : 0); 8249 const sin = Math.sin(angle); 8250 const cos = Math.cos(angle); 8251 vecEastX = cos; 8252 vecEastY = sin; 8253 vecSouthX = -sin; 8254 vecSouthY = cos; 8255 } else if (!rotateWithMap && pitchWithMap) { 8256 const skew = getTileSkewVectors(this.transform); 8257 vecEastX = skew.vecEast[0]; 8258 vecEastY = skew.vecEast[1]; 8259 vecSouthX = skew.vecSouth[0]; 8260 vecSouthY = skew.vecSouth[1]; 8261 } 8262 let basePointX = projectedPoint.x; 8263 let basePointY = projectedPoint.y; 8264 let distanceMultiplier = tileToViewport; 8265 if (pitchWithMap) { 8266 basePointX = translatedAnchorX; 8267 basePointY = translatedAnchorY; 8268 const zoomFraction = this.transform.zoom - tileID.overscaledZ; 8269 distanceMultiplier = Math.pow(2, -zoomFraction); 8270 distanceMultiplier *= this.transform.getPitchedTextCorrection(translatedAnchorX, translatedAnchorY, unwrappedTileID); 8271 if (!shift) { 8272 const distanceRatio = projectedPoint.signedDistanceFromCamera / this.transform.cameraToCenterDistance; 8273 const perspectiveRatio = clamp(.5 + .5 * distanceRatio, 0, 4); 8274 distanceMultiplier *= perspectiveRatio; 8275 } 8276 } 8277 if (shift) { 8278 basePointX += vecEastX * shift.x * distanceMultiplier + vecSouthX * shift.y * distanceMultiplier; 8279 basePointY += vecEastY * shift.x * distanceMultiplier + vecSouthY * shift.y * distanceMultiplier; 8280 } 8281 const offsetXmin = collisionBox.x1 * distanceMultiplier; 8282 const offsetXmax = collisionBox.x2 * distanceMultiplier; 8283 const offsetXhalf = (offsetXmin + offsetXmax) / 2; 8284 const offsetYmin = collisionBox.y1 * distanceMultiplier; 8285 const offsetYmax = collisionBox.y2 * distanceMultiplier; 8286 const offsetYhalf = (offsetYmin + offsetYmax) / 2; 8287 const offsetsArray = [ 8288 { 8289 offsetX: offsetXmin, 8290 offsetY: offsetYmin 8291 }, 8292 { 8293 offsetX: offsetXhalf, 8294 offsetY: offsetYmin 8295 }, 8296 { 8297 offsetX: offsetXmax, 8298 offsetY: offsetYmin 8299 }, 8300 { 8301 offsetX: offsetXmax, 8302 offsetY: offsetYhalf 8303 }, 8304 { 8305 offsetX: offsetXmax, 8306 offsetY: offsetYmax 8307 }, 8308 { 8309 offsetX: offsetXhalf, 8310 offsetY: offsetYmax 8311 }, 8312 { 8313 offsetX: offsetXmin, 8314 offsetY: offsetYmax 8315 }, 8316 { 8317 offsetX: offsetXmin, 8318 offsetY: offsetYhalf 8319 } 8320 ]; 8321 let points = []; 8322 for (const { offsetX, offsetY } of offsetsArray) points.push(new Point(basePointX + vecEastX * offsetX + vecSouthX * offsetY, basePointY + vecEastY * offsetX + vecSouthY * offsetY)); 8323 let anyPointVisible = false; 8324 if (pitchWithMap) { 8325 const projected = points.map((p) => this.projectAndGetPerspectiveRatio(p.x, p.y, unwrappedTileID, getSymbolElevation(getElevation, p.x, p.y, heightOffset, heightAnchorGround), simpleProjectionMatrix)); 8326 anyPointVisible = projected.some((p) => !p.isOccluded); 8327 points = projected.map((p) => new Point(p.x, p.y)); 8328 } else anyPointVisible = true; 8329 return { 8330 box: getAABB(points), 8331 allPointsOccluded: !anyPointVisible 8332 }; 8333 } 8334}; 8335//#endregion 8336//#region src/symbol/placement.ts 8337var OpacityState = class { 8338 constructor(prevState, increment, placed, skipFade) { 8339 if (prevState) this.opacity = Math.max(0, Math.min(1, prevState.opacity + (prevState.placed ? increment : -increment))); 8340 else this.opacity = skipFade && placed ? 1 : 0; 8341 this.placed = placed; 8342 } 8343 isHidden() { 8344 return this.opacity === 0 && !this.placed; 8345 } 8346}; 8347var JointOpacityState = class { 8348 constructor(prevState, increment, placedText, placedIcon, skipFade) { 8349 this.text = new OpacityState(prevState ? prevState.text : null, increment, placedText, skipFade); 8350 this.icon = new OpacityState(prevState ? prevState.icon : null, increment, placedIcon, skipFade); 8351 } 8352 isHidden() { 8353 return this.text.isHidden() && this.icon.isHidden(); 8354 } 8355}; 8356var JointPlacement = class { 8357 constructor(text, icon, skipFade) { 8358 this.text = text; 8359 this.icon = icon; 8360 this.skipFade = skipFade; 8361 } 8362}; 8363var RetainedQueryData = class { 8364 constructor(bucketInstanceId, featureIndex, sourceLayerIndex, bucketIndex, tileID) { 8365 this.bucketInstanceId = bucketInstanceId; 8366 this.featureIndex = featureIndex; 8367 this.sourceLayerIndex = sourceLayerIndex; 8368 this.bucketIndex = bucketIndex; 8369 this.tileID = tileID; 8370 } 8371}; 8372var CollisionGroups = class { 8373 constructor(crossSourceCollisions) { 8374 this.crossSourceCollisions = crossSourceCollisions; 8375 this.maxGroupID = 0; 8376 this.collisionGroups = {}; 8377 } 8378 get(sourceID) { 8379 if (!this.crossSourceCollisions) { 8380 if (!this.collisionGroups[sourceID]) { 8381 const nextGroupID = ++this.maxGroupID; 8382 this.collisionGroups[sourceID] = { 8383 ID: nextGroupID, 8384 predicate: (key) => { 8385 return key.collisionGroupID === nextGroupID; 8386 } 8387 }; 8388 } 8389 return this.collisionGroups[sourceID]; 8390 } else return { 8391 ID: 0, 8392 predicate: null 8393 }; 8394 } 8395}; 8396function calculateVariableLayoutShift(anchor, width, height, textOffset, textBoxScale) { 8397 const { horizontalAlign, verticalAlign } = getAnchorAlignment(anchor); 8398 const shiftX = -(horizontalAlign - .5) * width; 8399 const shiftY = -(verticalAlign - .5) * height; 8400 return new Point(shiftX + textOffset[0] * textBoxScale, shiftY + textOffset[1] * textBoxScale); 8401} 8402var Placement = class { 8403 constructor(transform, terrain, fadeDuration, crossSourceCollisions, prevPlacement) { 8404 this.transform = transform.clone(); 8405 this.terrain = terrain; 8406 this.collisionIndex = new CollisionIndex(this.transform); 8407 this.placements = {}; 8408 this.opacities = {}; 8409 this.variableOffsets = {}; 8410 this.stale = false; 8411 this.commitTime = 0; 8412 this.fadeDuration = fadeDuration; 8413 this.retainedQueryData = {}; 8414 this.collisionGroups = new CollisionGroups(crossSourceCollisions); 8415 this.collisionCircleArrays = {}; 8416 this.collisionBoxArrays = /* @__PURE__ */ new Map(); 8417 this.prevPlacement = prevPlacement; 8418 if (prevPlacement) prevPlacement.prevPlacement = void 0; 8419 this.placedOrientations = {}; 8420 } 8421 _getTerrainElevationFunc(tileID) { 8422 const terrain = this.terrain; 8423 if (!terrain) return void 0; 8424 return (x, y) => terrain.getElevation(tileID, x, y); 8425 } 8426 getBucketParts(results, styleLayer, tile, sortAcrossTiles) { 8427 const symbolBucket = tile.getBucket(styleLayer); 8428 const bucketFeatureIndex = tile.latestFeatureIndex; 8429 if (!symbolBucket || !bucketFeatureIndex || styleLayer.id !== symbolBucket.layerIds[0]) return; 8430 const collisionBoxArray = tile.collisionBoxArray; 8431 const layout = symbolBucket.layers[0].layout; 8432 const paint = symbolBucket.layers[0].paint; 8433 const scale = Math.pow(2, this.transform.zoom - tile.tileID.overscaledZ); 8434 const textPixelRatio = tile.tileSize / EXTENT; 8435 const unwrappedTileID = tile.tileID.toUnwrapped(); 8436 const rotateWithMap = layout.get("text-rotation-alignment") === "map"; 8437 const pixelsToTiles = pixelsToTileUnits(tile, 1, this.transform.zoom); 8438 const translationText = translatePosition(this.collisionIndex.transform, tile, paint.get("text-translate"), paint.get("text-translate-anchor")); 8439 const translationIcon = translatePosition(this.collisionIndex.transform, tile, paint.get("icon-translate"), paint.get("icon-translate-anchor")); 8440 const pitchedLabelPlaneMatrix = getPitchedLabelPlaneMatrix(rotateWithMap, this.transform, pixelsToTiles); 8441 this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData(symbolBucket.bucketInstanceId, bucketFeatureIndex, symbolBucket.sourceLayerIndex, symbolBucket.index, tile.tileID); 8442 const parameters = { 8443 bucket: symbolBucket, 8444 layout, 8445 translationText, 8446 translationIcon, 8447 unwrappedTileID, 8448 pitchedLabelPlaneMatrix, 8449 scale, 8450 textPixelRatio, 8451 holdingForFade: tile.holdingForSymbolFade(), 8452 collisionBoxArray, 8453 partiallyEvaluatedTextSize: evaluateSizeForZoom(symbolBucket.textSizeData, this.transform.zoom), 8454 collisionGroup: this.collisionGroups.get(symbolBucket.sourceID) 8455 }; 8456 if (sortAcrossTiles) for (const range of symbolBucket.sortKeyRanges) { 8457 const { sortKey, symbolInstanceStart, symbolInstanceEnd } = range; 8458 results.push({ 8459 sortKey, 8460 symbolInstanceStart, 8461 symbolInstanceEnd, 8462 parameters 8463 }); 8464 } 8465 else results.push({ 8466 symbolInstanceStart: 0, 8467 symbolInstanceEnd: symbolBucket.symbolInstances.length, 8468 parameters 8469 }); 8470 } 8471 attemptAnchorPlacement(textAnchorOffset, textBox, width, height, textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, tileID, unwrappedTileID, collisionGroup, textOverlapMode, symbolInstance, bucket, orientation, translationText, translationIcon, iconBox, getElevation, simpleProjectionMatrix, heightOffset, heightAnchorGround) { 8472 const anchor = TextAnchorEnum[textAnchorOffset.textAnchor]; 8473 const textOffset = [textAnchorOffset.textOffset0, textAnchorOffset.textOffset1]; 8474 const shift = calculateVariableLayoutShift(anchor, width, height, textOffset, textBoxScale); 8475 const placedGlyphBoxes = this.collisionIndex.placeCollisionBox(textBox, textOverlapMode, textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translationText, collisionGroup.predicate, getElevation, shift, simpleProjectionMatrix, heightOffset, heightAnchorGround); 8476 if (iconBox) { 8477 if (!this.collisionIndex.placeCollisionBox(iconBox, textOverlapMode, textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translationIcon, collisionGroup.predicate, getElevation, shift, simpleProjectionMatrix, heightOffset, heightAnchorGround).placeable) return; 8478 } 8479 if (placedGlyphBoxes.placeable) { 8480 let prevAnchor; 8481 if (this.prevPlacement?.variableOffsets[symbolInstance.crossTileID] && this.prevPlacement?.placements[symbolInstance.crossTileID]?.text) prevAnchor = this.prevPlacement.variableOffsets[symbolInstance.crossTileID].anchor; 8482 if (symbolInstance.crossTileID === 0) throw new Error("symbolInstance.crossTileID can't be 0"); 8483 this.variableOffsets[symbolInstance.crossTileID] = { 8484 textOffset, 8485 width, 8486 height, 8487 anchor, 8488 textBoxScale, 8489 prevAnchor 8490 }; 8491 this.markUsedJustification(bucket, anchor, symbolInstance, orientation); 8492 if (bucket.allowVerticalPlacement) { 8493 this.markUsedOrientation(bucket, orientation, symbolInstance); 8494 this.placedOrientations[symbolInstance.crossTileID] = orientation; 8495 } 8496 return { 8497 shift, 8498 placedGlyphBoxes 8499 }; 8500 } 8501 } 8502 placeLayerBucketPart(bucketPart, seenCrossTileIDs, showCollisionBoxes) { 8503 const { bucket, layout, translationText, translationIcon, unwrappedTileID, pitchedLabelPlaneMatrix, textPixelRatio, holdingForFade, collisionBoxArray, partiallyEvaluatedTextSize, collisionGroup } = bucketPart.parameters; 8504 const textOptional = layout.get("text-optional"); 8505 const iconOptional = layout.get("icon-optional"); 8506 const textOverlapMode = getOverlapMode(layout, "text-overlap", "text-allow-overlap"); 8507 const textAlwaysOverlap = textOverlapMode === "always"; 8508 const iconOverlapMode = getOverlapMode(layout, "icon-overlap", "icon-allow-overlap"); 8509 const iconAlwaysOverlap = iconOverlapMode === "always"; 8510 const rotateWithMap = layout.get("text-rotation-alignment") === "map"; 8511 const pitchWithMap = layout.get("text-pitch-alignment") === "map"; 8512 const hasIconTextFit = layout.get("icon-text-fit") !== "none"; 8513 const zOrderByViewportY = layout.get("symbol-z-order") === "viewport-y"; 8514 const heightAnchorGround = layout.get("symbol-height-anchor") === "ground"; 8515 const alwaysShowText = textAlwaysOverlap && (iconAlwaysOverlap || !bucket.hasIconData() || iconOptional); 8516 const alwaysShowIcon = iconAlwaysOverlap && (textAlwaysOverlap || !bucket.hasTextData() || textOptional); 8517 if (!bucket.collisionArrays && collisionBoxArray) bucket.deserializeCollisionBoxes(collisionBoxArray); 8518 const tileID = this.retainedQueryData[bucket.bucketInstanceId].tileID; 8519 const getElevation = this._getTerrainElevationFunc(tileID); 8520 const simpleProjectionMatrix = this.transform.getFastPathSimpleProjectionMatrix(tileID); 8521 const placeSymbol = (symbolInstance, collisionArrays, symbolIndex) => { 8522 if (seenCrossTileIDs[symbolInstance.crossTileID]) return; 8523 if (holdingForFade) { 8524 this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false); 8525 return; 8526 } 8527 const symbolHeightOffset = symbolInstance.heightOffset; 8528 let placeText = false; 8529 let placeIcon = false; 8530 let offscreen = true; 8531 let shift = null; 8532 let placed = { 8533 box: null, 8534 placeable: false, 8535 offscreen: null, 8536 occluded: false 8537 }; 8538 let placedVerticalText = { 8539 box: null, 8540 placeable: false, 8541 offscreen: null 8542 }; 8543 let placedGlyphBoxes = null; 8544 let placedGlyphCircles = null; 8545 let placedIconBoxes = null; 8546 let textFeatureIndex = 0; 8547 let verticalTextFeatureIndex = 0; 8548 let iconFeatureIndex = 0; 8549 if (collisionArrays.textFeatureIndex) textFeatureIndex = collisionArrays.textFeatureIndex; 8550 else if (symbolInstance.useRuntimeCollisionCircles) textFeatureIndex = symbolInstance.featureIndex; 8551 if (collisionArrays.verticalTextFeatureIndex) verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex; 8552 const textBox = collisionArrays.textBox; 8553 if (textBox) { 8554 const updatePreviousOrientationIfNotPlaced = (isPlaced) => { 8555 let previousOrientation = 1; 8556 if (bucket.allowVerticalPlacement && !isPlaced && this.prevPlacement) { 8557 const prevPlacedOrientation = this.prevPlacement.placedOrientations[symbolInstance.crossTileID]; 8558 if (prevPlacedOrientation) { 8559 this.placedOrientations[symbolInstance.crossTileID] = prevPlacedOrientation; 8560 previousOrientation = prevPlacedOrientation; 8561 this.markUsedOrientation(bucket, previousOrientation, symbolInstance); 8562 } 8563 } 8564 return previousOrientation; 8565 }; 8566 const placeTextForPlacementModes = (placeHorizontalFn, placeVerticalFn) => { 8567 if (bucket.allowVerticalPlacement && symbolInstance.numVerticalGlyphVertices > 0 && collisionArrays.verticalTextBox) for (const placementMode of bucket.writingModes) { 8568 if (placementMode === 2) { 8569 placed = placeVerticalFn(); 8570 placedVerticalText = placed; 8571 } else placed = placeHorizontalFn(); 8572 if (placed?.placeable) break; 8573 } 8574 else placed = placeHorizontalFn(); 8575 }; 8576 const textAnchorOffsetStart = symbolInstance.textAnchorOffsetStartIndex; 8577 const textAnchorOffsetEnd = symbolInstance.textAnchorOffsetEndIndex; 8578 if (textAnchorOffsetEnd === textAnchorOffsetStart) { 8579 const placeBox = (collisionTextBox, orientation) => { 8580 const placedFeature = this.collisionIndex.placeCollisionBox(collisionTextBox, textOverlapMode, textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translationText, collisionGroup.predicate, getElevation, void 0, simpleProjectionMatrix, symbolHeightOffset, heightAnchorGround); 8581 if (placedFeature?.placeable) { 8582 this.markUsedOrientation(bucket, orientation, symbolInstance); 8583 this.placedOrientations[symbolInstance.crossTileID] = orientation; 8584 } 8585 return placedFeature; 8586 }; 8587 const placeHorizontal = () => { 8588 return placeBox(textBox, 1); 8589 }; 8590 const placeVertical = () => { 8591 const verticalTextBox = collisionArrays.verticalTextBox; 8592 if (bucket.allowVerticalPlacement && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) return placeBox(verticalTextBox, 2); 8593 return { 8594 box: null, 8595 offscreen: null 8596 }; 8597 }; 8598 placeTextForPlacementModes(placeHorizontal, placeVertical); 8599 updatePreviousOrientationIfNotPlaced(placed?.placeable); 8600 } else { 8601 let prevAnchor = TextAnchorEnum[this.prevPlacement?.variableOffsets[symbolInstance.crossTileID]?.anchor]; 8602 const placeBoxForVariableAnchors = (collisionTextBox, collisionIconBox, orientation) => { 8603 const width = collisionTextBox.x2 - collisionTextBox.x1; 8604 const height = collisionTextBox.y2 - collisionTextBox.y1; 8605 const textBoxScale = symbolInstance.textBoxScale; 8606 const variableIconBox = hasIconTextFit && iconOverlapMode === "never" ? collisionIconBox : null; 8607 let placedBox = null; 8608 let placementPasses = textOverlapMode === "never" ? 1 : 2; 8609 let overlapMode = "never"; 8610 if (prevAnchor) placementPasses++; 8611 for (let pass = 0; pass < placementPasses; pass++) { 8612 for (let i = textAnchorOffsetStart; i < textAnchorOffsetEnd; i++) { 8613 const textAnchorOffset = bucket.textAnchorOffsets.get(i); 8614 if (prevAnchor && textAnchorOffset.textAnchor !== prevAnchor) continue; 8615 const result = this.attemptAnchorPlacement(textAnchorOffset, collisionTextBox, width, height, textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, tileID, unwrappedTileID, collisionGroup, overlapMode, symbolInstance, bucket, orientation, translationText, translationIcon, variableIconBox, getElevation, simpleProjectionMatrix, symbolHeightOffset, heightAnchorGround); 8616 if (result) { 8617 placedBox = result.placedGlyphBoxes; 8618 if (placedBox?.placeable) { 8619 placeText = true; 8620 shift = result.shift; 8621 return placedBox; 8622 } 8623 } 8624 } 8625 if (prevAnchor) prevAnchor = null; 8626 else overlapMode = textOverlapMode; 8627 } 8628 if (showCollisionBoxes && !placedBox) placedBox = { 8629 box: this.collisionIndex.placeCollisionBox(textBox, "always", textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translationText, collisionGroup.predicate, getElevation, void 0, simpleProjectionMatrix, symbolHeightOffset, heightAnchorGround).box, 8630 offscreen: false, 8631 placeable: false, 8632 occluded: false 8633 }; 8634 return placedBox; 8635 }; 8636 const placeHorizontal = () => { 8637 return placeBoxForVariableAnchors(textBox, collisionArrays.iconBox, 1); 8638 }; 8639 const placeVertical = () => { 8640 const verticalTextBox = collisionArrays.verticalTextBox; 8641 const wasPlaced = placed?.placeable; 8642 if (bucket.allowVerticalPlacement && !wasPlaced && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) return placeBoxForVariableAnchors(verticalTextBox, collisionArrays.verticalIconBox, 2); 8643 return { 8644 box: null, 8645 occluded: true, 8646 offscreen: null 8647 }; 8648 }; 8649 placeTextForPlacementModes(placeHorizontal, placeVertical); 8650 if (placed) { 8651 placeText = placed.placeable; 8652 offscreen = placed.offscreen; 8653 } 8654 const prevOrientation = updatePreviousOrientationIfNotPlaced(placed?.placeable); 8655 if (!placeText && this.prevPlacement) { 8656 const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID]; 8657 if (prevOffset) { 8658 this.variableOffsets[symbolInstance.crossTileID] = prevOffset; 8659 this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance, prevOrientation); 8660 } 8661 } 8662 } 8663 } 8664 placedGlyphBoxes = placed; 8665 placeText = placedGlyphBoxes?.placeable; 8666 offscreen = placedGlyphBoxes?.offscreen; 8667 if (symbolInstance.useRuntimeCollisionCircles && symbolInstance.centerJustifiedTextSymbolIndex >= 0) { 8668 const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex); 8669 const fontSize = evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol); 8670 const textPixelPadding = layout.get("text-padding"); 8671 const circlePixelDiameter = symbolInstance.collisionCircleDiameter; 8672 placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textOverlapMode, placedSymbol, bucket.lineVertexArray, bucket.glyphOffsetArray, fontSize, unwrappedTileID, pitchedLabelPlaneMatrix, showCollisionBoxes, pitchWithMap, collisionGroup.predicate, circlePixelDiameter, textPixelPadding, translationText, getElevation); 8673 if (placedGlyphCircles.circles.length && placedGlyphCircles.collisionDetected && !showCollisionBoxes) warnOnce("Collisions detected, but collision boxes are not shown"); 8674 placeText = textAlwaysOverlap || placedGlyphCircles.circles.length > 0 && !placedGlyphCircles.collisionDetected; 8675 offscreen &&= placedGlyphCircles.offscreen; 8676 } 8677 if (collisionArrays.iconFeatureIndex) iconFeatureIndex = collisionArrays.iconFeatureIndex; 8678 if (collisionArrays.iconBox) { 8679 const placeIconFeature = (iconBox) => { 8680 return this.collisionIndex.placeCollisionBox(iconBox, iconOverlapMode, textPixelRatio, tileID, unwrappedTileID, pitchWithMap, rotateWithMap, translationIcon, collisionGroup.predicate, getElevation, hasIconTextFit && shift ? shift : void 0, simpleProjectionMatrix, symbolHeightOffset, heightAnchorGround); 8681 }; 8682 if (placedVerticalText && placedVerticalText.placeable && collisionArrays.verticalIconBox) { 8683 placedIconBoxes = placeIconFeature(collisionArrays.verticalIconBox); 8684 placeIcon = placedIconBoxes.placeable; 8685 } else { 8686 placedIconBoxes = placeIconFeature(collisionArrays.iconBox); 8687 placeIcon = placedIconBoxes.placeable; 8688 } 8689 offscreen &&= placedIconBoxes.offscreen; 8690 } 8691 const iconWithoutText = textOptional || symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0; 8692 const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0; 8693 if (!iconWithoutText && !textWithoutIcon) placeIcon = placeText = placeIcon && placeText; 8694 else if (!textWithoutIcon) placeText = placeIcon && placeText; 8695 else if (!iconWithoutText) placeIcon &&= placeText; 8696 const hasTextBox = placeText && placedGlyphBoxes.placeable; 8697 const hasIconBox = placeIcon && placedIconBoxes.placeable; 8698 if (hasTextBox) { 8699 if (placedVerticalText && placedVerticalText.placeable && verticalTextFeatureIndex) this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, textOverlapMode, layout.get("text-ignore-placement"), bucket.bucketInstanceId, verticalTextFeatureIndex, collisionGroup.ID); 8700 else this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, textOverlapMode, layout.get("text-ignore-placement"), bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID); 8701 } 8702 if (hasIconBox) this.collisionIndex.insertCollisionBox(placedIconBoxes.box, iconOverlapMode, layout.get("icon-ignore-placement"), bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID); 8703 if (placedGlyphCircles) { 8704 if (placeText) this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, textOverlapMode, layout.get("text-ignore-placement"), bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID); 8705 } 8706 if (showCollisionBoxes) this.storeCollisionData(bucket.bucketInstanceId, symbolIndex, collisionArrays, placedGlyphBoxes, placedIconBoxes, placedGlyphCircles); 8707 if (symbolInstance.crossTileID === 0) throw new Error("symbolInstance.crossTileID can't be 0"); 8708 if (bucket.bucketInstanceId === 0) throw new Error("bucket.bucketInstanceId can't be 0"); 8709 const textVisible = (placeText || alwaysShowText) && !placedGlyphBoxes?.occluded; 8710 const iconVisible = (placeIcon || alwaysShowIcon) && !placedIconBoxes?.occluded; 8711 this.placements[symbolInstance.crossTileID] = new JointPlacement(textVisible, iconVisible, offscreen || bucket.justReloaded); 8712 seenCrossTileIDs[symbolInstance.crossTileID] = true; 8713 }; 8714 if (zOrderByViewportY) { 8715 if (bucketPart.symbolInstanceStart !== 0) throw new Error("bucket.bucketInstanceId should be 0"); 8716 const symbolIndexes = bucket.getSortedSymbolIndexes(-this.transform.bearingInRadians); 8717 for (let i = symbolIndexes.length - 1; i >= 0; --i) { 8718 const symbolIndex = symbolIndexes[i]; 8719 placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex], symbolIndex); 8720 } 8721 } else for (let i = bucketPart.symbolInstanceStart; i < bucketPart.symbolInstanceEnd; i++) placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i], i); 8722 bucket.justReloaded = false; 8723 } 8724 storeCollisionData(bucketInstanceId, symbolIndex, collisionArrays, placedGlyphBoxes, placedIconBoxes, placedGlyphCircles) { 8725 if (collisionArrays.textBox || collisionArrays.iconBox) { 8726 let boxArray; 8727 if (this.collisionBoxArrays.has(bucketInstanceId)) boxArray = this.collisionBoxArrays.get(bucketInstanceId); 8728 else { 8729 boxArray = /* @__PURE__ */ new Map(); 8730 this.collisionBoxArrays.set(bucketInstanceId, boxArray); 8731 } 8732 let realCollisionBox; 8733 if (boxArray.has(symbolIndex)) realCollisionBox = boxArray.get(symbolIndex); 8734 else { 8735 realCollisionBox = { 8736 text: null, 8737 icon: null 8738 }; 8739 boxArray.set(symbolIndex, realCollisionBox); 8740 } 8741 if (collisionArrays.textBox) realCollisionBox.text = placedGlyphBoxes.box; 8742 if (collisionArrays.iconBox) realCollisionBox.icon = placedIconBoxes.box; 8743 } 8744 if (placedGlyphCircles) { 8745 let circleArray = this.collisionCircleArrays[bucketInstanceId]; 8746 if (circleArray === void 0) circleArray = this.collisionCircleArrays[bucketInstanceId] = []; 8747 for (let i = 0; i < placedGlyphCircles.circles.length; i += 4) { 8748 circleArray.push(placedGlyphCircles.circles[i + 0] - 100); 8749 circleArray.push(placedGlyphCircles.circles[i + 1] - 100); 8750 circleArray.push(placedGlyphCircles.circles[i + 2]); 8751 circleArray.push(placedGlyphCircles.collisionDetected ? 1 : 0); 8752 } 8753 } 8754 } 8755 markUsedJustification(bucket, placedAnchor, symbolInstance, orientation) { 8756 const justifications = { 8757 "left": symbolInstance.leftJustifiedTextSymbolIndex, 8758 "center": symbolInstance.centerJustifiedTextSymbolIndex, 8759 "right": symbolInstance.rightJustifiedTextSymbolIndex 8760 }; 8761 let autoIndex; 8762 if (orientation === 2) autoIndex = symbolInstance.verticalPlacedTextSymbolIndex; 8763 else autoIndex = justifications[getAnchorJustification(placedAnchor)]; 8764 const indexes = [ 8765 symbolInstance.leftJustifiedTextSymbolIndex, 8766 symbolInstance.centerJustifiedTextSymbolIndex, 8767 symbolInstance.rightJustifiedTextSymbolIndex, 8768 symbolInstance.verticalPlacedTextSymbolIndex 8769 ]; 8770 for (const index of indexes) if (index >= 0) { 8771 if (autoIndex >= 0 && index !== autoIndex) bucket.text.placedSymbolArray.get(index).crossTileID = 0; 8772 else bucket.text.placedSymbolArray.get(index).crossTileID = symbolInstance.crossTileID; 8773 } 8774 } 8775 markUsedOrientation(bucket, orientation, symbolInstance) { 8776 const horizontal = orientation === 1 || orientation === 3 ? orientation : 0; 8777 const vertical = orientation === 2 ? orientation : 0; 8778 const horizontalIndexes = [ 8779 symbolInstance.leftJustifiedTextSymbolIndex, 8780 symbolInstance.centerJustifiedTextSymbolIndex, 8781 symbolInstance.rightJustifiedTextSymbolIndex 8782 ]; 8783 for (const index of horizontalIndexes) bucket.text.placedSymbolArray.get(index).placedOrientation = horizontal; 8784 if (symbolInstance.verticalPlacedTextSymbolIndex) bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).placedOrientation = vertical; 8785 } 8786 commit(now) { 8787 this.commitTime = now; 8788 this.zoomAtLastRecencyCheck = this.transform.zoom; 8789 const prevPlacement = this.prevPlacement; 8790 let placementChanged = false; 8791 this.prevZoomAdjustment = prevPlacement ? prevPlacement.zoomAdjustment(this.transform.zoom) : 0; 8792 const increment = prevPlacement ? prevPlacement.symbolFadeChange(now) : 1; 8793 const prevOpacities = prevPlacement ? prevPlacement.opacities : {}; 8794 const prevOffsets = prevPlacement ? prevPlacement.variableOffsets : {}; 8795 const prevOrientations = prevPlacement ? prevPlacement.placedOrientations : {}; 8796 for (const crossTileID in this.placements) { 8797 const jointPlacement = this.placements[crossTileID]; 8798 const prevOpacity = prevOpacities[crossTileID]; 8799 if (prevOpacity) { 8800 this.opacities[crossTileID] = new JointOpacityState(prevOpacity, increment, jointPlacement.text, jointPlacement.icon); 8801 placementChanged ||= jointPlacement.text !== prevOpacity.text.placed; 8802 placementChanged ||= jointPlacement.icon !== prevOpacity.icon.placed; 8803 } else { 8804 this.opacities[crossTileID] = new JointOpacityState(null, increment, jointPlacement.text, jointPlacement.icon, jointPlacement.skipFade); 8805 placementChanged ||= jointPlacement.text || jointPlacement.icon; 8806 } 8807 } 8808 for (const crossTileID in prevOpacities) { 8809 const prevOpacity = prevOpacities[crossTileID]; 8810 if (!this.opacities[crossTileID]) { 8811 const jointOpacity = new JointOpacityState(prevOpacity, increment, false, false); 8812 if (!jointOpacity.isHidden()) { 8813 this.opacities[crossTileID] = jointOpacity; 8814 placementChanged ||= prevOpacity.text.placed; 8815 placementChanged ||= prevOpacity.icon.placed; 8816 } 8817 } 8818 } 8819 for (const crossTileID in prevOffsets) if (!this.variableOffsets[crossTileID] && this.opacities[crossTileID] && !this.opacities[crossTileID].isHidden()) this.variableOffsets[crossTileID] = prevOffsets[crossTileID]; 8820 for (const crossTileID in prevOrientations) if (!this.placedOrientations[crossTileID] && this.opacities[crossTileID] && !this.opacities[crossTileID].isHidden()) this.placedOrientations[crossTileID] = prevOrientations[crossTileID]; 8821 if (prevPlacement && prevPlacement.lastPlacementChangeTime === void 0) throw new Error("Last placement time for previous placement is not defined"); 8822 if (placementChanged) this.lastPlacementChangeTime = now; 8823 else if (typeof this.lastPlacementChangeTime !== "number") this.lastPlacementChangeTime = prevPlacement ? prevPlacement.lastPlacementChangeTime : now; 8824 } 8825 updateLayerOpacities(styleLayer, tiles) { 8826 const seenCrossTileIDs = {}; 8827 for (const tile of tiles) { 8828 const symbolBucket = tile.getBucket(styleLayer); 8829 if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layerIds[0]) this.updateBucketOpacities(symbolBucket, tile.tileID, seenCrossTileIDs, tile.collisionBoxArray); 8830 } 8831 } 8832 updateBucketOpacities(bucket, tileID, seenCrossTileIDs, collisionBoxArray) { 8833 if (bucket.hasTextData()) { 8834 bucket.text.opacityVertexArray.clear(); 8835 bucket.text.hasVisibleVertices = false; 8836 } 8837 if (bucket.hasIconData()) { 8838 bucket.icon.opacityVertexArray.clear(); 8839 bucket.icon.hasVisibleVertices = false; 8840 } 8841 if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear(); 8842 if (bucket.hasTextCollisionBoxData()) bucket.textCollisionBox.collisionVertexArray.clear(); 8843 const layer = bucket.layers[0]; 8844 const layout = layer.layout; 8845 const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true); 8846 const textAllowOverlap = layout.get("text-allow-overlap"); 8847 const iconAllowOverlap = layout.get("icon-allow-overlap"); 8848 const hasVariablePlacement = layer._unevaluatedLayout.hasValue("text-variable-anchor") || layer._unevaluatedLayout.hasValue("text-variable-anchor-offset"); 8849 const rotateWithMap = layout.get("text-rotation-alignment") === "map"; 8850 const pitchWithMap = layout.get("text-pitch-alignment") === "map"; 8851 const hasIconTextFit = layout.get("icon-text-fit") !== "none"; 8852 const defaultOpacityState = new JointOpacityState(null, 0, textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || layout.get("icon-optional")), iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get("text-optional")), true); 8853 if (!bucket.collisionArrays && collisionBoxArray && (bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData())) bucket.deserializeCollisionBoxes(collisionBoxArray); 8854 const addOpacities = (iconOrText, numVertices, opacity) => { 8855 for (let i = 0; i < numVertices / 4; i++) iconOrText.opacityVertexArray.emplaceBack(opacity); 8856 iconOrText.hasVisibleVertices ||= opacity !== PACKED_HIDDEN_OPACITY; 8857 }; 8858 const boxArrays = this.collisionBoxArrays.get(bucket.bucketInstanceId); 8859 for (let s = 0; s < bucket.symbolInstances.length; s++) { 8860 const symbolInstance = bucket.symbolInstances.get(s); 8861 const { numHorizontalGlyphVertices, numVerticalGlyphVertices, crossTileID } = symbolInstance; 8862 const isDuplicate = seenCrossTileIDs[crossTileID]; 8863 let opacityState = this.opacities[crossTileID]; 8864 if (isDuplicate) opacityState = duplicateOpacityState; 8865 else if (!opacityState) { 8866 opacityState = defaultOpacityState; 8867 this.opacities[crossTileID] = opacityState; 8868 } 8869 seenCrossTileIDs[crossTileID] = true; 8870 const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0; 8871 const hasIcon = symbolInstance.numIconVertices > 0; 8872 const placedOrientation = this.placedOrientations[symbolInstance.crossTileID]; 8873 const horizontalHidden = placedOrientation === 2; 8874 const verticalHidden = placedOrientation === 1 || placedOrientation === 3; 8875 if (hasText) { 8876 const packedOpacity = packOpacity(opacityState.text); 8877 const horizontalOpacity = horizontalHidden ? PACKED_HIDDEN_OPACITY : packedOpacity; 8878 addOpacities(bucket.text, numHorizontalGlyphVertices, horizontalOpacity); 8879 const verticalOpacity = verticalHidden ? PACKED_HIDDEN_OPACITY : packedOpacity; 8880 addOpacities(bucket.text, numVerticalGlyphVertices, verticalOpacity); 8881 const symbolHidden = opacityState.text.isHidden(); 8882 const textSymbolIndexes = [ 8883 symbolInstance.rightJustifiedTextSymbolIndex, 8884 symbolInstance.centerJustifiedTextSymbolIndex, 8885 symbolInstance.leftJustifiedTextSymbolIndex 8886 ]; 8887 for (const index of textSymbolIndexes) if (index >= 0) bucket.text.placedSymbolArray.get(index).hidden = symbolHidden || horizontalHidden ? 1 : 0; 8888 if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).hidden = symbolHidden || verticalHidden ? 1 : 0; 8889 const prevOffset = this.variableOffsets[symbolInstance.crossTileID]; 8890 if (prevOffset) this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance, placedOrientation); 8891 const prevOrientation = this.placedOrientations[symbolInstance.crossTileID]; 8892 if (prevOrientation) { 8893 this.markUsedJustification(bucket, "left", symbolInstance, prevOrientation); 8894 this.markUsedOrientation(bucket, prevOrientation, symbolInstance); 8895 } 8896 } 8897 if (hasIcon) { 8898 const packedOpacity = packOpacity(opacityState.icon); 8899 const useHorizontal = !(hasIconTextFit && symbolInstance.verticalPlacedIconSymbolIndex && horizontalHidden); 8900 if (symbolInstance.placedIconSymbolIndex >= 0) { 8901 const horizontalOpacity = useHorizontal ? packedOpacity : PACKED_HIDDEN_OPACITY; 8902 addOpacities(bucket.icon, symbolInstance.numIconVertices, horizontalOpacity); 8903 bucket.icon.placedSymbolArray.get(symbolInstance.placedIconSymbolIndex).hidden = opacityState.icon.isHidden(); 8904 } 8905 if (symbolInstance.verticalPlacedIconSymbolIndex >= 0) { 8906 const verticalOpacity = !useHorizontal ? packedOpacity : PACKED_HIDDEN_OPACITY; 8907 addOpacities(bucket.icon, symbolInstance.numVerticalIconVertices, verticalOpacity); 8908 bucket.icon.placedSymbolArray.get(symbolInstance.verticalPlacedIconSymbolIndex).hidden = opacityState.icon.isHidden(); 8909 } 8910 } 8911 const realBoxes = boxArrays?.has(s) ? boxArrays.get(s) : { 8912 text: null, 8913 icon: null 8914 }; 8915 if (bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData()) { 8916 const collisionArrays = bucket.collisionArrays[s]; 8917 if (collisionArrays) { 8918 let shift = new Point(0, 0); 8919 if (collisionArrays.textBox || collisionArrays.verticalTextBox) { 8920 let used = true; 8921 if (hasVariablePlacement) { 8922 const variableOffset = this.variableOffsets[crossTileID]; 8923 if (variableOffset) { 8924 shift = calculateVariableLayoutShift(variableOffset.anchor, variableOffset.width, variableOffset.height, variableOffset.textOffset, variableOffset.textBoxScale); 8925 if (rotateWithMap) shift._rotate(pitchWithMap ? -this.transform.bearingInRadians : this.transform.bearingInRadians); 8926 } else used = false; 8927 } 8928 if (collisionArrays.textBox || collisionArrays.verticalTextBox) { 8929 let hidden; 8930 if (collisionArrays.textBox) hidden = horizontalHidden; 8931 if (collisionArrays.verticalTextBox) hidden = verticalHidden; 8932 updateCollisionVertices(bucket.textCollisionBox.collisionVertexArray, opacityState.text.placed, !used || hidden, realBoxes.text, shift.x, shift.y); 8933 } 8934 } 8935 if (collisionArrays.iconBox || collisionArrays.verticalIconBox) { 8936 const verticalIconUsed = Boolean(!verticalHidden && collisionArrays.verticalIconBox); 8937 let hidden; 8938 if (collisionArrays.iconBox) hidden = verticalIconUsed; 8939 if (collisionArrays.verticalIconBox) hidden = !verticalIconUsed; 8940 updateCollisionVertices(bucket.iconCollisionBox.collisionVertexArray, opacityState.icon.placed, hidden, realBoxes.icon, hasIconTextFit ? shift.x : 0, hasIconTextFit ? shift.y : 0); 8941 } 8942 } 8943 } 8944 } 8945 bucket.sortFeatures(-this.transform.bearingInRadians); 8946 if (this.retainedQueryData[bucket.bucketInstanceId]) this.retainedQueryData[bucket.bucketInstanceId].featureSortOrder = bucket.featureSortOrder; 8947 if (bucket.hasTextData() && bucket.text.opacityVertexBuffer) bucket.text.opacityVertexBuffer.updateData(bucket.text.opacityVertexArray); 8948 if (bucket.hasIconData() && bucket.icon.opacityVertexBuffer) bucket.icon.opacityVertexBuffer.updateData(bucket.icon.opacityVertexArray); 8949 if (bucket.hasIconCollisionBoxData() && bucket.iconCollisionBox.collisionVertexBuffer) bucket.iconCollisionBox.collisionVertexBuffer.updateData(bucket.iconCollisionBox.collisionVertexArray); 8950 if (bucket.hasTextCollisionBoxData() && bucket.textCollisionBox.collisionVertexBuffer) bucket.textCollisionBox.collisionVertexBuffer.updateData(bucket.textCollisionBox.collisionVertexArray); 8951 if (bucket.text.opacityVertexArray.length !== bucket.text.layoutVertexArray.length / 4) throw new Error(`bucket.text.opacityVertexArray.length (= ${bucket.text.opacityVertexArray.length}) !== bucket.text.layoutVertexArray.length (= ${bucket.text.layoutVertexArray.length}) / 4`); 8952 if (bucket.icon.opacityVertexArray.length !== bucket.icon.layoutVertexArray.length / 4) throw new Error(`bucket.icon.opacityVertexArray.length (= ${bucket.icon.opacityVertexArray.length}) !== bucket.icon.layoutVertexArray.length (= ${bucket.icon.layoutVertexArray.length}) / 4`); 8953 if (bucket.bucketInstanceId in this.collisionCircleArrays) { 8954 bucket.collisionCircleArray = this.collisionCircleArrays[bucket.bucketInstanceId]; 8955 delete this.collisionCircleArrays[bucket.bucketInstanceId]; 8956 } 8957 } 8958 symbolFadeChange(now) { 8959 return this.fadeDuration === 0 ? 1 : (now - this.commitTime) / this.fadeDuration + this.prevZoomAdjustment; 8960 } 8961 zoomAdjustment(zoom) { 8962 return Math.max(0, (this.transform.zoom - zoom) / 1.5); 8963 } 8964 hasTransitions(now) { 8965 return this.stale || now - this.lastPlacementChangeTime < this.fadeDuration; 8966 } 8967 stillRecent(now, zoom) { 8968 const durationAdjustment = this.zoomAtLastRecencyCheck === zoom ? 1 - this.zoomAdjustment(zoom) : 1; 8969 this.zoomAtLastRecencyCheck = zoom; 8970 return this.commitTime + this.fadeDuration * durationAdjustment > now; 8971 } 8972 setStale() { 8973 this.stale = true; 8974 } 8975}; 8976function updateCollisionVertices(collisionVertexArray, placed, notUsed, realBox, shiftX, shiftY) { 8977 if (!realBox || realBox.length === 0) realBox = [ 8978 0, 8979 0, 8980 0, 8981 0 8982 ]; 8983 const tlX = realBox[0] - 100; 8984 const tlY = realBox[1] - 100; 8985 const brX = realBox[2] - 100; 8986 const brY = realBox[3] - 100; 8987 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0, tlX, tlY); 8988 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0, brX, tlY); 8989 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0, brX, brY); 8990 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0, tlX, brY); 8991} 8992const shift25 = Math.pow(2, 25); 8993const shift24 = Math.pow(2, 24); 8994const shift17 = Math.pow(2, 17); 8995const shift16 = Math.pow(2, 16); 8996const shift9 = Math.pow(2, 9); 8997const shift8 = Math.pow(2, 8); 8998const shift1 = Math.pow(2, 1); 8999function packOpacity(opacityState) { 9000 if (opacityState.opacity === 0 && !opacityState.placed) return 0; 9001 else if (opacityState.opacity === 1 && opacityState.placed) return 4294967295; 9002 const targetBit = opacityState.placed ? 1 : 0; 9003 const opacityBits = Math.floor(opacityState.opacity * 127); 9004 return opacityBits * shift25 + targetBit * shift24 + opacityBits * shift17 + targetBit * shift16 + opacityBits * shift9 + targetBit * shift8 + opacityBits * shift1 + targetBit; 9005} 9006const PACKED_HIDDEN_OPACITY = 0; 9007//#endregion 9008//#region src/style/pauseable_placement.ts 9009var LayerPlacement = class { 9010 constructor(styleLayer) { 9011 this._sortAcrossTiles = styleLayer.layout.get("symbol-z-order") !== "viewport-y" && !styleLayer.layout.get("symbol-sort-key").isConstant(); 9012 this._currentTileIndex = 0; 9013 this._currentPartIndex = 0; 9014 this._seenCrossTileIDs = {}; 9015 this._bucketParts = []; 9016 } 9017 continuePlacement(tiles, placement, showCollisionBoxes, styleLayer, shouldPausePlacement) { 9018 const bucketParts = this._bucketParts; 9019 while (this._currentTileIndex < tiles.length) { 9020 const tile = tiles[this._currentTileIndex]; 9021 placement.getBucketParts(bucketParts, styleLayer, tile, this._sortAcrossTiles); 9022 this._currentTileIndex++; 9023 if (shouldPausePlacement()) return true; 9024 } 9025 if (this._sortAcrossTiles) { 9026 this._sortAcrossTiles = false; 9027 bucketParts.sort((a, b) => a.sortKey - b.sortKey); 9028 } 9029 while (this._currentPartIndex < bucketParts.length) { 9030 const bucketPart = bucketParts[this._currentPartIndex]; 9031 placement.placeLayerBucketPart(bucketPart, this._seenCrossTileIDs, showCollisionBoxes); 9032 this._currentPartIndex++; 9033 if (shouldPausePlacement()) return true; 9034 } 9035 return false; 9036 } 9037}; 9038var PauseablePlacement = class { 9039 constructor(transform, terrain, order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, prevPlacement) { 9040 this.placement = new Placement(transform, terrain, fadeDuration, crossSourceCollisions, prevPlacement); 9041 this._currentPlacementIndex = order.length - 1; 9042 this._forceFullPlacement = forceFullPlacement; 9043 this._showCollisionBoxes = showCollisionBoxes; 9044 this._done = false; 9045 } 9046 isDone() { 9047 return this._done; 9048 } 9049 continuePlacement(order, layers, layerTiles) { 9050 const startTime = now(); 9051 const shouldPausePlacement = () => { 9052 return this._forceFullPlacement ? false : now() - startTime > 2; 9053 }; 9054 while (this._currentPlacementIndex >= 0) { 9055 const layer = layers[order[this._currentPlacementIndex]]; 9056 const placementZoom = this.placement.collisionIndex.transform.zoom; 9057 if (isSymbolStyleLayer(layer) && layer.layout && (!layer.minzoom || layer.minzoom <= placementZoom) && (!layer.maxzoom || layer.maxzoom > placementZoom)) { 9058 this._inProgressLayer ||= new LayerPlacement(layer); 9059 if (this._inProgressLayer.continuePlacement(layerTiles[layer.source], this.placement, this._showCollisionBoxes, layer, shouldPausePlacement)) return; 9060 delete this._inProgressLayer; 9061 } 9062 this._currentPlacementIndex--; 9063 } 9064 this._done = true; 9065 } 9066 commit(now) { 9067 this.placement.commit(now); 9068 return this.placement; 9069 } 9070}; 9071//#endregion 9072//#region node_modules/kdbush/index.js 9073const ARRAY_TYPES = [ 9074 Int8Array, 9075 Uint8Array, 9076 Uint8ClampedArray, 9077 Int16Array, 9078 Uint16Array, 9079 Int32Array, 9080 Uint32Array, 9081 Float32Array, 9082 Float64Array 9083]; 9084/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */ 9085/** @typedef {Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array} TypedArray */ 9086const VERSION = 1; 9087const HEADER_SIZE = 8; 9088const STACK = /* @__PURE__ */ new Uint32Array(96); 9089var KDBush = class KDBush { 9090 /** 9091 * Creates an index from raw `ArrayBuffer` data. 9092 * @param {ArrayBufferLike} data 9093 */ 9094 static from(data) { 9095 if (!data || data.byteLength === void 0 || data.buffer) throw new Error("Data must be an instance of ArrayBuffer or SharedArrayBuffer."); 9096 const [magic, versionAndType] = new Uint8Array(data, 0, 2); 9097 if (magic !== 219) throw new Error("Data does not appear to be in a KDBush format."); 9098 const version = versionAndType >> 4; 9099 if (version !== VERSION) throw new Error(`Got v${version} data when expected v${VERSION}.`); 9100 const ArrayType = ARRAY_TYPES[versionAndType & 15]; 9101 if (!ArrayType) throw new Error("Unrecognized array type."); 9102 const [nodeSize] = new Uint16Array(data, 2, 1); 9103 const [numItems] = new Uint32Array(data, 4, 1); 9104 return new KDBush(numItems, nodeSize, ArrayType, void 0, data); 9105 } 9106 /** 9107 * Creates an index that will hold a given number of items. 9108 * @param {number} numItems 9109 * @param {number} [nodeSize=64] Size of the KD-tree node (64 by default). 9110 * @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default). 9111 * @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used for storage (`ArrayBuffer` by default). 9112 * @param {ArrayBufferLike} [data] (For internal use only) 9113 */ 9114 constructor(numItems, nodeSize = 64, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data) { 9115 if (isNaN(numItems) || numItems < 0) throw new Error(`Unexpected numItems value: ${numItems}.`); 9116 this.numItems = +numItems; 9117 this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535); 9118 this.ArrayType = ArrayType; 9119 this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array; 9120 const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType); 9121 const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT; 9122 const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT; 9123 const padCoords = (8 - idsByteSize % 8) % 8; 9124 if (arrayTypeIndex < 0) throw new Error(`Unexpected typed array class: ${ArrayType}.`); 9125 if (data) { 9126 this.data = data; 9127 this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems); 9128 this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2); 9129 this._pos = numItems * 2; 9130 this._finished = true; 9131 } else { 9132 const data = this.data = new ArrayBufferType(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords); 9133 this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems); 9134 this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2); 9135 this._pos = 0; 9136 this._finished = false; 9137 new Uint8Array(data, 0, 2).set([219, 16 + arrayTypeIndex]); 9138 new Uint16Array(data, 2, 1)[0] = nodeSize; 9139 new Uint32Array(data, 4, 1)[0] = numItems; 9140 } 9141 } 9142 /** 9143 * Add a point to the index. 9144 * @param {number} x 9145 * @param {number} y 9146 * @returns {number} An incremental index associated with the added item (starting from `0`). 9147 */ 9148 add(x, y) { 9149 const index = this._pos >> 1; 9150 this.ids[index] = index; 9151 this.coords[this._pos++] = x; 9152 this.coords[this._pos++] = y; 9153 return index; 9154 } 9155 /** 9156 * Perform indexing of the added points. 9157 */ 9158 finish() { 9159 const numAdded = this._pos >> 1; 9160 if (numAdded !== this.numItems) throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`); 9161 sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0); 9162 this._finished = true; 9163 return this; 9164 } 9165 /** 9166 * Search the index for items within a given bounding box. 9167 * @param {number} minX 9168 * @param {number} minY 9169 * @param {number} maxX 9170 * @param {number} maxY 9171 * @returns {number[]} An array of indices correponding to the found items. 9172 */ 9173 range(minX, minY, maxX, maxY) { 9174 if (!this._finished) throw new Error("Data not yet indexed - call index.finish()."); 9175 const { ids, coords, nodeSize } = this; 9176 STACK[0] = 0; 9177 STACK[1] = ids.length - 1; 9178 STACK[2] = 0; 9179 let sp = 3; 9180 const result = []; 9181 while (sp > 0) { 9182 const axis = STACK[--sp]; 9183 const right = STACK[--sp]; 9184 const left = STACK[--sp]; 9185 if (right - left <= nodeSize) { 9186 for (let i = left; i <= right; i++) { 9187 const x = coords[2 * i]; 9188 const y = coords[2 * i + 1]; 9189 if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]); 9190 } 9191 continue; 9192 } 9193 const m = left + right >> 1; 9194 const x = coords[2 * m]; 9195 const y = coords[2 * m + 1]; 9196 if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]); 9197 if (axis === 0 ? minX <= x : minY <= y) { 9198 STACK[sp++] = left; 9199 STACK[sp++] = m - 1; 9200 STACK[sp++] = 1 - axis; 9201 } 9202 if (axis === 0 ? maxX >= x : maxY >= y) { 9203 STACK[sp++] = m + 1; 9204 STACK[sp++] = right; 9205 STACK[sp++] = 1 - axis; 9206 } 9207 } 9208 return result; 9209 } 9210 /** 9211 * Search the index for items within a given radius. 9212 * @param {number} qx 9213 * @param {number} qy 9214 * @param {number} r Query radius. 9215 * @returns {number[]} An array of indices correponding to the found items. 9216 */ 9217 within(qx, qy, r) { 9218 const result = []; 9219 this.withinInto(qx, qy, r, result); 9220 return result; 9221 } 9222 /** 9223 * Search the index for items within a given radius, writing matching ids into `out` 9224 * via indexed assignment (`out[i] = id`). Accepts any indexed-writable container â 9225 * a typed array sized to the expected upper bound (allocation-free, fast) or a plain 9226 * `Array` (which will grow as needed). Returns the number of matches written. 9227 * @param {number} qx 9228 * @param {number} qy 9229 * @param {number} r Query radius. 9230 * @param {number[] | TypedArray} out Container to write matching ids into. 9231 * @returns {number} The number of matches written to `out`. 9232 */ 9233 withinInto(qx, qy, r, out) { 9234 if (!this._finished) throw new Error("Data not yet indexed - call index.finish()."); 9235 const { ids, coords, nodeSize } = this; 9236 STACK[0] = 0; 9237 STACK[1] = ids.length - 1; 9238 STACK[2] = 0; 9239 let sp = 3; 9240 let count = 0; 9241 const r2 = r * r; 9242 while (sp > 0) { 9243 const axis = STACK[--sp]; 9244 const right = STACK[--sp]; 9245 const left = STACK[--sp]; 9246 if (right - left <= nodeSize) { 9247 for (let i = left; i <= right; i++) if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) out[count++] = ids[i]; 9248 continue; 9249 } 9250 const m = left + right >> 1; 9251 const x = coords[2 * m]; 9252 const y = coords[2 * m + 1]; 9253 if (sqDist(x, y, qx, qy) <= r2) out[count++] = ids[m]; 9254 if (axis === 0 ? qx - r <= x : qy - r <= y) { 9255 STACK[sp++] = left; 9256 STACK[sp++] = m - 1; 9257 STACK[sp++] = 1 - axis; 9258 } 9259 if (axis === 0 ? qx + r >= x : qy + r >= y) { 9260 STACK[sp++] = m + 1; 9261 STACK[sp++] = right; 9262 STACK[sp++] = 1 - axis; 9263 } 9264 } 9265 return count; 9266 } 9267}; 9268/** 9269* @param {Uint16Array | Uint32Array} ids 9270* @param {TypedArray} coords 9271* @param {number} nodeSize 9272* @param {number} left 9273* @param {number} right 9274* @param {number} axis 9275*/ 9276function sort(ids, coords, nodeSize, left, right, axis) { 9277 if (right - left <= nodeSize) return; 9278 const m = left + right >> 1; 9279 select(ids, coords, m, left, right, axis); 9280 sort(ids, coords, nodeSize, left, m - 1, 1 - axis); 9281 sort(ids, coords, nodeSize, m + 1, right, 1 - axis); 9282} 9283/** 9284* Custom Floyd-Rivest selection algorithm: sort ids and coords so that 9285* [left..k-1] items are smaller than k-th item (on either x or y axis) 9286* @param {Uint16Array | Uint32Array} ids 9287* @param {TypedArray} coords 9288* @param {number} k 9289* @param {number} left 9290* @param {number} right 9291* @param {number} axis 9292*/ 9293function select(ids, coords, k, left, right, axis) { 9294 while (right > left) { 9295 if (right - left > 600) { 9296 const n = right - left + 1; 9297 const m = k - left + 1; 9298 const z = Math.log(n); 9299 const s = .5 * Math.exp(2 * z / 3); 9300 const sd = .5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); 9301 select(ids, coords, k, Math.max(left, Math.floor(k - m * s / n + sd)), Math.min(right, Math.floor(k + (n - m) * s / n + sd)), axis); 9302 } 9303 const t = coords[2 * k + axis]; 9304 let i = left; 9305 let j = right; 9306 swapItem(ids, coords, left, k); 9307 if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right); 9308 while (i < j) { 9309 swapItem(ids, coords, i, j); 9310 i++; 9311 j--; 9312 while (coords[2 * i + axis] < t) i++; 9313 while (coords[2 * j + axis] > t) j--; 9314 } 9315 if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j); 9316 else { 9317 j++; 9318 swapItem(ids, coords, j, right); 9319 } 9320 if (j <= k) left = j + 1; 9321 if (k <= j) right = j - 1; 9322 } 9323} 9324/** 9325* @param {Uint16Array | Uint32Array} ids 9326* @param {TypedArray} coords 9327* @param {number} i 9328* @param {number} j 9329*/ 9330function swapItem(ids, coords, i, j) { 9331 swap(ids, i, j); 9332 swap(coords, 2 * i, 2 * j); 9333 swap(coords, 2 * i + 1, 2 * j + 1); 9334} 9335/** 9336* @param {TypedArray} arr 9337* @param {number} i 9338* @param {number} j 9339*/ 9340function swap(arr, i, j) { 9341 const tmp = arr[i]; 9342 arr[i] = arr[j]; 9343 arr[j] = tmp; 9344} 9345/** 9346* @param {number} ax 9347* @param {number} ay 9348* @param {number} bx 9349* @param {number} by 9350*/ 9351function sqDist(ax, ay, bx, by) { 9352 const dx = ax - bx; 9353 const dy = ay - by; 9354 return dx * dx + dy * dy; 9355} 9356//#endregion 9357//#region src/symbol/cross_tile_symbol_index.ts 9358const roundingFactor = 512 / EXTENT / 2; 9359var TileLayerIndex = class { 9360 constructor(tileID, symbolInstances, bucketInstanceId) { 9361 this.tileID = tileID; 9362 this.bucketInstanceId = bucketInstanceId; 9363 this._symbolsByKey = {}; 9364 const symbolInstancesByKey = /* @__PURE__ */ new Map(); 9365 for (let i = 0; i < symbolInstances.length; i++) { 9366 const symbolInstance = symbolInstances.get(i); 9367 const key = symbolInstance.key; 9368 const instances = symbolInstancesByKey.get(key); 9369 if (instances) instances.push(symbolInstance); 9370 else symbolInstancesByKey.set(key, [symbolInstance]); 9371 } 9372 for (const [key, symbols] of symbolInstancesByKey) { 9373 const entry = { 9374 positions: symbols.map((symbolInstance) => ({ 9375 x: Math.floor(symbolInstance.anchorX * roundingFactor), 9376 y: Math.floor(symbolInstance.anchorY * roundingFactor) 9377 })), 9378 crossTileIDs: symbols.map((v) => v.crossTileID) 9379 }; 9380 if (entry.positions.length > 128) { 9381 const index = new KDBush(entry.positions.length, 16, Uint16Array); 9382 for (const { x, y } of entry.positions) index.add(x, y); 9383 index.finish(); 9384 delete entry.positions; 9385 entry.index = index; 9386 } 9387 this._symbolsByKey[key] = entry; 9388 } 9389 } 9390 getScaledCoordinates(symbolInstance, childTileID) { 9391 const { x: localX, y: localY, z: localZ } = this.tileID.canonical; 9392 const { x, y, z } = childTileID.canonical; 9393 const zDifference = z - localZ; 9394 const scale = roundingFactor / Math.pow(2, zDifference); 9395 const xWorld = (x * EXTENT + symbolInstance.anchorX) * scale; 9396 const yWorld = (y * EXTENT + symbolInstance.anchorY) * scale; 9397 const xOffset = localX * EXTENT * roundingFactor; 9398 const yOffset = localY * EXTENT * roundingFactor; 9399 return { 9400 x: Math.floor(xWorld - xOffset), 9401 y: Math.floor(yWorld - yOffset) 9402 }; 9403 } 9404 findMatches(symbolInstances, newTileID, zoomCrossTileIDs) { 9405 const tolerance = this.tileID.canonical.z < newTileID.canonical.z ? 1 : Math.pow(2, this.tileID.canonical.z - newTileID.canonical.z); 9406 for (let i = 0; i < symbolInstances.length; i++) { 9407 const symbolInstance = symbolInstances.get(i); 9408 if (symbolInstance.crossTileID) continue; 9409 const entry = this._symbolsByKey[symbolInstance.key]; 9410 if (!entry) continue; 9411 const scaledSymbolCoord = this.getScaledCoordinates(symbolInstance, newTileID); 9412 if (entry.index) this.findMatchesForIndexedEntry(entry, symbolInstance, scaledSymbolCoord, tolerance, zoomCrossTileIDs); 9413 else if (entry.positions) this.findMatchesForNonIndexedEntry(entry, symbolInstance, scaledSymbolCoord, tolerance, zoomCrossTileIDs); 9414 } 9415 } 9416 /** 9417 * Matches a symbol instance against an entry backed by a KDBush spatial index 9418 * (used for keys with more than {@link KDBUSH_THRESHHOLD} symbols). 9419 * 9420 * Matches any symbol with the same key whose coordinates are within one grid 9421 * unit (with a 4px grid, this covers a 12px by 12px area). The lowest-index 9422 * unclaimed candidate is claimed in a single pass rather than sorting the 9423 * whole result set on every query: `range()` can return many candidates where 9424 * symbols are coincident (e.g. stacked point markers), so a per-query sort 9425 * would dominate placement cost on dense layers. 9426 * 9427 * Once a symbol is marked duplicate against a parent symbol, no other symbol 9428 * at the same zoom level may duplicate against the same parent (see issue #5993). 9429 */ 9430 findMatchesForIndexedEntry(entry, symbolInstance, scaledSymbolCoord, tolerance, zoomCrossTileIDs) { 9431 const indexes = entry.index.range(scaledSymbolCoord.x - tolerance, scaledSymbolCoord.y - tolerance, scaledSymbolCoord.x + tolerance, scaledSymbolCoord.y + tolerance); 9432 let bestIndex = Infinity; 9433 for (const candidate of indexes) if (candidate < bestIndex && !zoomCrossTileIDs[entry.crossTileIDs[candidate]]) bestIndex = candidate; 9434 if (bestIndex !== Infinity) { 9435 const crossTileID = entry.crossTileIDs[bestIndex]; 9436 zoomCrossTileIDs[crossTileID] = true; 9437 symbolInstance.crossTileID = crossTileID; 9438 } 9439 } 9440 /** 9441 * Matches a symbol instance against an entry that stores its positions as a 9442 * plain array (used for keys with at most {@link KDBUSH_THRESHHOLD} symbols). 9443 * 9444 * Matches any symbol with the same key whose coordinates are within one grid 9445 * unit (with a 4px grid, this covers a 12px by 12px area). Positions are 9446 * stored in symbol-instance order, so the first unclaimed match is also the 9447 * lowest-index one. 9448 * 9449 * Once a symbol is marked duplicate against a parent symbol, no other symbol 9450 * at the same zoom level may duplicate against the same parent (see issue #5993). 9451 */ 9452 findMatchesForNonIndexedEntry(entry, symbolInstance, scaledSymbolCoord, tolerance, zoomCrossTileIDs) { 9453 for (let i = 0; i < entry.positions.length; i++) { 9454 const thisTileSymbol = entry.positions[i]; 9455 const crossTileID = entry.crossTileIDs[i]; 9456 if (Math.abs(thisTileSymbol.x - scaledSymbolCoord.x) <= tolerance && Math.abs(thisTileSymbol.y - scaledSymbolCoord.y) <= tolerance && !zoomCrossTileIDs[crossTileID]) { 9457 zoomCrossTileIDs[crossTileID] = true; 9458 symbolInstance.crossTileID = crossTileID; 9459 break; 9460 } 9461 } 9462 } 9463 getCrossTileIDsLists() { 9464 return Object.values(this._symbolsByKey).map(({ crossTileIDs }) => crossTileIDs); 9465 } 9466}; 9467var CrossTileIDs = class { 9468 constructor() { 9469 this.maxCrossTileID = 0; 9470 } 9471 generate() { 9472 return ++this.maxCrossTileID; 9473 } 9474}; 9475var CrossTileSymbolLayerIndex = class { 9476 constructor() { 9477 this.indexes = {}; 9478 this.usedCrossTileIDs = {}; 9479 this.lng = 0; 9480 } 9481 handleWrapJump(lng) { 9482 const wrapDelta = Math.round((lng - this.lng) / 360); 9483 if (wrapDelta !== 0) for (const zoom in this.indexes) { 9484 const zoomIndexes = this.indexes[zoom]; 9485 const newZoomIndex = {}; 9486 for (const key in zoomIndexes) { 9487 const index = zoomIndexes[key]; 9488 index.tileID = index.tileID.unwrapTo(index.tileID.wrap + wrapDelta); 9489 newZoomIndex[index.tileID.key] = index; 9490 } 9491 this.indexes[zoom] = newZoomIndex; 9492 } 9493 this.lng = lng; 9494 } 9495 addBucket(tileID, bucket, crossTileIDs) { 9496 if (this.indexes[tileID.overscaledZ]?.[tileID.key]) { 9497 if (this.indexes[tileID.overscaledZ][tileID.key].bucketInstanceId === bucket.bucketInstanceId) return false; 9498 else this.removeBucketCrossTileIDs(tileID.overscaledZ, this.indexes[tileID.overscaledZ][tileID.key]); 9499 } 9500 for (let i = 0; i < bucket.symbolInstances.length; i++) { 9501 const symbolInstance = bucket.symbolInstances.get(i); 9502 symbolInstance.crossTileID = 0; 9503 } 9504 this.usedCrossTileIDs[tileID.overscaledZ] ||= {}; 9505 const zoomCrossTileIDs = this.usedCrossTileIDs[tileID.overscaledZ]; 9506 for (const zoom in this.indexes) { 9507 const zoomIndexes = this.indexes[zoom]; 9508 if (Number(zoom) > tileID.overscaledZ) for (const id in zoomIndexes) { 9509 const childIndex = zoomIndexes[id]; 9510 if (childIndex.tileID.isChildOf(tileID)) childIndex.findMatches(bucket.symbolInstances, tileID, zoomCrossTileIDs); 9511 } 9512 else { 9513 const parentIndex = zoomIndexes[tileID.scaledTo(Number(zoom)).key]; 9514 if (parentIndex) parentIndex.findMatches(bucket.symbolInstances, tileID, zoomCrossTileIDs); 9515 } 9516 } 9517 for (let i = 0; i < bucket.symbolInstances.length; i++) { 9518 const symbolInstance = bucket.symbolInstances.get(i); 9519 if (!symbolInstance.crossTileID) { 9520 symbolInstance.crossTileID = crossTileIDs.generate(); 9521 zoomCrossTileIDs[symbolInstance.crossTileID] = true; 9522 } 9523 } 9524 if (this.indexes[tileID.overscaledZ] === void 0) this.indexes[tileID.overscaledZ] = {}; 9525 this.indexes[tileID.overscaledZ][tileID.key] = new TileLayerIndex(tileID, bucket.symbolInstances, bucket.bucketInstanceId); 9526 return true; 9527 } 9528 removeBucketCrossTileIDs(zoom, removedBucket) { 9529 for (const crossTileIDs of removedBucket.getCrossTileIDsLists()) for (const crossTileID of crossTileIDs) delete this.usedCrossTileIDs[zoom][crossTileID]; 9530 } 9531 removeStaleBuckets(currentIDs) { 9532 let tilesChanged = false; 9533 for (const z in this.indexes) { 9534 const zoomIndexes = this.indexes[z]; 9535 for (const tileKey in zoomIndexes) if (!currentIDs[zoomIndexes[tileKey].bucketInstanceId]) { 9536 this.removeBucketCrossTileIDs(z, zoomIndexes[tileKey]); 9537 delete zoomIndexes[tileKey]; 9538 tilesChanged = true; 9539 } 9540 } 9541 return tilesChanged; 9542 } 9543}; 9544var CrossTileSymbolIndex = class { 9545 constructor() { 9546 this.layerIndexes = {}; 9547 this.crossTileIDs = new CrossTileIDs(); 9548 this.maxBucketInstanceId = 0; 9549 this.bucketsInCurrentPlacement = {}; 9550 } 9551 addLayer(styleLayer, tiles, lng) { 9552 let layerIndex = this.layerIndexes[styleLayer.id]; 9553 if (layerIndex === void 0) layerIndex = this.layerIndexes[styleLayer.id] = new CrossTileSymbolLayerIndex(); 9554 let symbolBucketsChanged = false; 9555 const currentBucketIDs = {}; 9556 layerIndex.handleWrapJump(lng); 9557 for (const tile of tiles) { 9558 const symbolBucket = tile.getBucket(styleLayer); 9559 if (styleLayer.id !== symbolBucket?.layerIds[0]) continue; 9560 if (!symbolBucket.bucketInstanceId) { 9561 this.maxBucketInstanceId += 1; 9562 symbolBucket.bucketInstanceId = this.maxBucketInstanceId; 9563 } 9564 if (layerIndex.addBucket(tile.tileID, symbolBucket, this.crossTileIDs)) symbolBucketsChanged = true; 9565 currentBucketIDs[symbolBucket.bucketInstanceId] = true; 9566 } 9567 if (layerIndex.removeStaleBuckets(currentBucketIDs)) symbolBucketsChanged = true; 9568 return symbolBucketsChanged; 9569 } 9570 pruneUnusedLayers(usedLayers) { 9571 const usedLayerMap = {}; 9572 for (const usedLayer of usedLayers) usedLayerMap[usedLayer] = true; 9573 for (const layerId in this.layerIndexes) if (!usedLayerMap[layerId]) delete this.layerIndexes[layerId]; 9574 } 9575}; 9576//#endregion 9577//#region src/shaders/glsl/_prelude.fragment.glsl.g.ts 9578var _prelude_fragment_glsl_g_default = "#ifdef GL_ES\nprecision mediump float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\nout highp vec4 fragColor;layout(std140) uniform ProjectionUBO {highp mat4 u_projection_matrix;highp mat4 u_projection_fallback_matrix;highp vec4 u_projection_tile_mercator_coords;highp vec4 u_projection_clipping_plane;highp float u_projection_transition;highp int u_projection_clip_antimeridian;};layout(std140) uniform FrameUBO {highp vec2 u_units_to_pixels;highp vec2 u_world_size;highp float u_camera_to_center_distance;highp float u_symbol_fade_change;highp float u_aspect_ratio;highp float u_device_pixel_ratio;highp vec2 u_viewport_size;highp vec2 u_pixel_extrude_scale;highp float u_pitch;};"; 9579//#endregion 9580//#region src/shaders/glsl/_prelude.vertex.glsl.g.ts 9581var _prelude_vertex_glsl_g_default = "#ifdef GL_ES\nprecision highp float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\nvec2 unpack_float(const float packedValue) {int packedIntValue=int(packedValue);int v0=packedIntValue/256;return vec2(v0,packedIntValue-v0*256);}vec2 unpack_opacity(const uint packedOpacity) {return vec2(float(packedOpacity >> 1u)/127.0,float(packedOpacity & 1u));}vec4 decode_color(const vec2 encodedColor) {return vec4(unpack_float(encodedColor[0])/255.0,unpack_float(encodedColor[1])/255.0\n);}float unpack_mix_vec2(const vec2 packedValue,const float t) {return mix(packedValue[0],packedValue[1],t);}vec4 unpack_mix_color(const vec4 packedColors,const float t) {vec4 minColor=decode_color(vec2(packedColors[0],packedColors[1]));vec4 maxColor=decode_color(vec2(packedColors[2],packedColors[3]));return mix(minColor,maxColor,t);}vec2 get_pattern_pos(const vec2 pixel_coord_upper,const vec2 pixel_coord_lower,const vec2 pattern_size,const float tile_units_to_pixels,const vec2 pos) {vec2 offset=mod(mod(mod(pixel_coord_upper,pattern_size)*256.0,pattern_size)*256.0+pixel_coord_lower,pattern_size);return (tile_units_to_pixels*pos+offset)/pattern_size;}mat3 rotationMatrixFromAxisAngle(vec3 u,float angle) {float c=cos(angle);float s=sin(angle);float c2=1.0-c;return mat3(u.x*u.x*c2+ c,u.x*u.y*c2-u.z*s,u.x*u.z*c2+u.y*s,u.y*u.x*c2+u.z*s,u.y*u.y*c2+ c,u.y*u.z*c2-u.x*s,u.z*u.x*c2-u.y*s,u.z*u.y*c2+u.x*s,u.z*u.z*c2+ c\n);}\n#ifdef TERRAIN3D\nuniform sampler2D u_terrain;uniform highp sampler2D u_depth;layout(std140) uniform TerrainUBO {highp mat4 u_terrain_matrix;highp vec4 u_terrain_unpack;highp float u_terrain_dim;highp float u_terrain_exaggeration;};\n#endif\nconst highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitShifts=vec4(1.)/bitSh;highp float unpack(highp vec4 color) {return dot(color,bitShifts);}highp float depthOpacity(vec3 frag) {\n#ifdef TERRAIN3D\nhighp float d=unpack(texture(u_depth,frag.xy*0.5+0.5))+0.0001-frag.z;return 1.0-max(0.0,min(1.0,-d*500.0));\n#else\nreturn 1.0;\n#endif\n}float calculate_visibility(vec4 pos) {\n#ifdef TERRAIN3D\nvec3 frag=pos.xyz/pos.w;highp float d=depthOpacity(frag);if (d > 0.95) return 1.0;return (d+depthOpacity(frag+vec3(0.0,0.01,0.0)))/2.0;\n#else\nreturn 1.0;\n#endif\n}float ele(ivec2 pos) {\n#ifdef TERRAIN3D\nvec4 rgb=(texelFetch(u_terrain,pos,0)*255.0)*u_terrain_unpack;return rgb.r+rgb.g+rgb.b-u_terrain_unpack.a;\n#else\nreturn 0.0;\n#endif\n}float get_elevation(vec2 pos) {\n#ifdef TERRAIN3D\n#ifdef GLOBE\nif ((pos.y <-32767.5) || (pos.y > 32766.5)) {return 0.0;}\n#endif\nvec2 coord=(u_terrain_matrix*vec4(pos,0.0,1.0)).xy*u_terrain_dim+1.5;vec2 f=fract(coord);ivec2 c=ivec2(floor(coord));ivec2 hi=textureSize(u_terrain,0)-1;float tl=ele(clamp(c,ivec2(0),hi));float tr=ele(clamp(c+ivec2(1,0),ivec2(0),hi));float bl=ele(clamp(c+ivec2(0,1),ivec2(0),hi));float br=ele(clamp(c+ivec2(1,1),ivec2(0),hi));float elevation=mix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);return elevation*u_terrain_exaggeration;\n#else\nreturn 0.0;\n#endif\n}const float PI=3.141592653589793;\n#define PROJECTION_UBO\nlayout(std140) uniform ProjectionUBO {highp mat4 u_projection_matrix;highp mat4 u_projection_fallback_matrix;highp vec4 u_projection_tile_mercator_coords;highp vec4 u_projection_clipping_plane;highp float u_projection_transition;highp int u_projection_clip_antimeridian;};layout(std140) uniform FrameUBO {highp vec2 u_units_to_pixels;highp vec2 u_world_size;highp float u_camera_to_center_distance;highp float u_symbol_fade_change;highp float u_aspect_ratio;highp float u_device_pixel_ratio;highp vec2 u_viewport_size;highp vec2 u_pixel_extrude_scale;highp float u_pitch;};"; 9582//#endregion 9583//#region src/shaders/glsl/background.fragment.glsl.g.ts 9584var background_fragment_glsl_g_default = "uniform vec4 u_color;uniform float u_opacity;void main() {fragColor=u_color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}"; 9585//#endregion 9586//#region src/shaders/glsl/background.vertex.glsl.g.ts 9587var background_vertex_glsl_g_default = "layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}"; 9588//#endregion 9589//#region src/shaders/glsl/background_pattern.fragment.glsl.g.ts 9590var background_pattern_fragment_glsl_g_default = "uniform vec2 u_pattern_tl_a;uniform vec2 u_pattern_br_a;uniform vec2 u_pattern_tl_b;uniform vec2 u_pattern_br_b;uniform vec2 u_texsize;uniform float u_mix;uniform float u_opacity;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;void main() {vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(u_pattern_tl_a/u_texsize,u_pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(u_pattern_tl_b/u_texsize,u_pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_mix)*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}"; 9591//#endregion 9592//#region src/shaders/glsl/background_pattern.vertex.glsl.g.ts 9593var background_pattern_vertex_glsl_g_default = "uniform vec2 u_pattern_size_a;uniform vec2 u_pattern_size_b;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_scale_a;uniform float u_scale_b;uniform float u_tile_units_to_pixels;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;void main() {gl_Position=projectTile(a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_a*u_pattern_size_a,u_tile_units_to_pixels,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_b*u_pattern_size_b,u_tile_units_to_pixels,a_pos);}"; 9594//#endregion 9595//#region src/shaders/glsl/circle.fragment.glsl.g.ts 9596var circle_fragment_glsl_g_default = "in vec3 v_data;flat in float v_visibility;\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define mediump float radius\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 stroke_color\n#pragma maplibre: define mediump float stroke_width\n#pragma maplibre: define lowp float stroke_opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump float radius\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 stroke_color\n#pragma maplibre: initialize mediump float stroke_width\n#pragma maplibre: initialize lowp float stroke_opacity\nvec2 extrude=v_data.xy;float extrude_length=length(extrude);float antialiased_blur=v_data.z;float opacity_t=smoothstep(0.0,antialiased_blur,extrude_length-1.0);float color_t=stroke_width < 0.01 ? 0.0 : smoothstep(antialiased_blur,0.0,extrude_length-radius/(radius+stroke_width));fragColor=v_visibility*opacity_t*mix(color*opacity,stroke_color*stroke_opacity,color_t);const float epsilon=0.5/255.0;if (fragColor.r < epsilon && fragColor.g < epsilon && fragColor.b < epsilon && fragColor.a < epsilon) {discard;}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}"; 9597//#endregion 9598//#region src/shaders/glsl/circle.vertex.glsl.g.ts 9599var circle_vertex_glsl_g_default = "uniform bool u_scale_with_map;uniform bool u_pitch_with_map;uniform vec2 u_extrude_scale;uniform highp float u_globe_extrude_scale;uniform vec2 u_translate;layout(location=0) in ivec2 a_pos;out vec3 v_data;flat out float v_visibility;\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define mediump float radius\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 stroke_color\n#pragma maplibre: define mediump float stroke_width\n#pragma maplibre: define lowp float stroke_opacity\nvoid main(void) {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump float radius\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 stroke_color\n#pragma maplibre: initialize mediump float stroke_width\n#pragma maplibre: initialize lowp float stroke_opacity\nivec2 pos_raw=a_pos+32768;vec2 extrude=vec2(pos_raw & 7)/7.0*2.0-1.0;vec2 circle_center=vec2(pos_raw >> 3)+u_translate;float ele=get_elevation(circle_center);v_visibility=calculate_visibility(projectTileWithElevation(circle_center,ele));if (u_pitch_with_map) {\n#ifdef GLOBE\nvec3 center_vector=projectToSphere(circle_center);\n#endif\nfloat angle_scale=u_globe_extrude_scale;vec2 corner_position=circle_center;if (u_scale_with_map) {angle_scale*=(radius+stroke_width);corner_position+=extrude*u_extrude_scale*(radius+stroke_width);} else {\n#ifdef GLOBE\nvec4 projected_center=interpolateProjection(circle_center,center_vector,ele);\n#else\nvec4 projected_center=projectTileWithElevation(circle_center,ele);\n#endif\ncorner_position+=extrude*u_extrude_scale*(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);angle_scale*=(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);}\n#ifdef GLOBE\nvec2 angles=extrude*angle_scale;vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(corner_position,corner_vector,ele);\n#else\ngl_Position=projectTileWithElevation(corner_position,ele);\n#endif\n} else {gl_Position=projectTileWithElevation(circle_center,ele);if (gl_Position.z/gl_Position.w > 1.0) {gl_Position.xy=vec2(10000.0);}if (u_scale_with_map) {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*u_camera_to_center_distance;} else {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*gl_Position.w;}}float antialiasblur=-max(1.0/u_device_pixel_ratio/(radius+stroke_width),blur);v_data=vec3(extrude.x,extrude.y,antialiasblur);}"; 9600//#endregion 9601//#region src/shaders/glsl/clipping_mask.fragment.glsl.g.ts 9602var clipping_mask_fragment_glsl_g_default = "void main() {fragColor=vec4(1.0);}"; 9603//#endregion 9604//#region src/shaders/shaders.ts 9605const shaders = { 9606 prelude: prepare(_prelude_fragment_glsl_g_default, _prelude_vertex_glsl_g_default), 9607 projectionMercator: prepare("\nvoid clipAntimeridian() {}", "float projectLineThickness(float tileY) {return 1.0;}float projectCircleRadius(float tileY) {return 1.0;}vec4 projectTile(vec2 p) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);return result;}vec4 projectTile(vec2 p,vec2 rawPos) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);if (rawPos.y <-32767.5 || rawPos.y > 32766.5) {result.z=-10000000.0;}return result;}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_projection_matrix*vec4(posInTile,elevation,1.0);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {return projectTileWithElevation(posInTile,elevation);}"), 9608 projectionGlobe: prepare("in highp float v_projection_tile_x;void clipAntimeridian() {if (u_projection_clip_antimeridian !=0 && (v_projection_tile_x < 0.0 || v_projection_tile_x >=8192.0)) {discard;}}", "#define GLOBE_RADIUS 6371008.8\n#ifndef PROJECTION_UBO\nuniform highp vec4 u_projection_tile_mercator_coords;uniform highp vec4 u_projection_clipping_plane;uniform highp float u_projection_transition;uniform mat4 u_projection_fallback_matrix;\n#endif\nout highp float v_projection_tile_x;vec3 globeRotateVector(vec3 vec,vec2 angles) {vec3 axisRight=vec3(vec.z,0.0,-vec.x);vec3 axisUp=cross(axisRight,vec);axisRight=normalize(axisRight);axisUp=normalize(axisUp);vec2 t=tan(angles);return normalize(vec+axisRight*t.x+axisUp*t.y);}mat3 globeGetRotationMatrix(vec3 spherePos) {vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);vec3 axisDown=cross(axisRight,spherePos);axisRight=normalize(axisRight);axisDown=normalize(axisDown);return mat3(axisRight,axisDown,spherePos\n);}float circumferenceRatioAtTileY(float tileY) {float mercator_pos_y=u_projection_tile_mercator_coords.y+u_projection_tile_mercator_coords.w*tileY;float t=exp(PI-(mercator_pos_y*PI*2.0));return (2.0*t)/(t*t+1.0);}float projectLineThickness(float tileY) {float thickness=1.0/circumferenceRatioAtTileY(tileY);if (u_projection_transition < 0.999) {return mix(1.0,thickness,u_projection_transition);} else {return thickness;}}vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {vec2 mercator_pos=u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;float spherical_x=mercator_pos.x*PI*2.0+PI;float t=exp(PI-(mercator_pos.y*PI*2.0));float t2=t*t;float denom=t2+1.0;float sin_sy=(t2-1.0)/denom;float cos_sy=(2.0*t)/denom;vec3 pos=vec3(sin(spherical_x)*cos_sy,sin_sy,cos(spherical_x)*cos_sy\n);if (rawPos.y <-32767.5) {pos=vec3(0.0,1.0,0.0);}if (rawPos.y > 32766.5) {pos=vec3(0.0,-1.0,0.0);}return pos;}vec3 projectToSphere(vec2 posInTile) {return projectToSphere(posInTile,vec2(0.0,0.0));}float globeComputeClippingZ(vec3 spherePos) {return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));}vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {v_projection_tile_x=posInTile.x;vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;if (u_projection_transition > 0.999) {return globePosition;}vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);const float z_globeness_threshold=0.2;vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {v_projection_tile_x=posInTile.x;vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);if (u_projection_transition > 0.999) {return globePosition;}vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);return mix(fallbackPosition,globePosition,u_projection_transition);}vec4 projectTile(vec2 posInTile) {return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);}vec4 projectTile(vec2 posInTile,vec2 rawPos) {return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {vec3 spherePos=projectToSphere(posInTile,posInTile);return interpolateProjectionFor3D(posInTile,spherePos,elevation);}"), 9609 background: prepare(background_fragment_glsl_g_default, background_vertex_glsl_g_default), 9610 backgroundPattern: prepare(background_pattern_fragment_glsl_g_default, background_pattern_vertex_glsl_g_default), 9611 circle: prepare(circle_fragment_glsl_g_default, circle_vertex_glsl_g_default), 9612 clippingMask: prepare(clipping_mask_fragment_glsl_g_default, "layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}"), 9613 heatmap: prepare("uniform highp float u_intensity;in vec2 v_extrude;\n#pragma maplibre: define highp float weight\n#define GAUSS_COEF 0.3989422804014327\nvoid main() {\n#pragma maplibre: initialize highp float weight\nfloat d=-0.5*3.0*3.0*dot(v_extrude,v_extrude);float val=weight*u_intensity*GAUSS_COEF*exp(d);fragColor=vec4(val,1.0,1.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform float u_extrude_scale;uniform float u_opacity;uniform float u_intensity;uniform highp float u_globe_extrude_scale;layout(location=0) in ivec2 a_pos;out vec2 v_extrude;\n#pragma maplibre: define highp float weight\n#pragma maplibre: define mediump float radius\nconst highp float ZERO=1.0/255.0/16.0;\n#define GAUSS_COEF 0.3989422804014327\nvoid main(void) {\n#pragma maplibre: initialize highp float weight\n#pragma maplibre: initialize mediump float radius\nivec2 pos_raw=a_pos+32768;vec2 unscaled_extrude=vec2(pos_raw & 7)/7.0*2.0-1.0;float S=sqrt(-2.0*log(ZERO/weight/u_intensity/GAUSS_COEF))/3.0;v_extrude=S*unscaled_extrude;vec2 extrude=v_extrude*radius*u_extrude_scale;vec2 circle_center=vec2(pos_raw >> 3);\n#ifdef GLOBE\nvec2 angles=v_extrude*radius*u_globe_extrude_scale;vec3 center_vector=projectToSphere(circle_center);vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(circle_center+extrude,corner_vector,0.0);\n#else\ngl_Position=projectTileFor3D(circle_center+extrude,get_elevation(circle_center));\n#endif\n}"), 9614 heatmapTexture: prepare("uniform sampler2D u_image;uniform sampler2D u_color_ramp;uniform float u_opacity;in vec2 v_pos;void main() {float t=texture(u_image,v_pos).r;vec4 color=texture(u_color_ramp,vec2(t,0.5));fragColor=color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(0.0);\n#endif\n}", "uniform mat4 u_matrix;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos*u_world_size,0,1);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}"), 9615 collisionBox: prepare("flat in float v_placed;flat in float v_notUsed;void main() {float alpha=0.5;fragColor=vec4(1.0,0.0,0.0,1.0)*alpha;if (v_placed > 0.5) {fragColor=vec4(0.0,0.0,1.0,0.5)*alpha;}if (v_notUsed > 0.5) {fragColor*=.1;}}", "layout(location=0) in vec2 a_anchor_pos;layout(location=1) in vec2 a_placed;layout(location=2) in vec2 a_box_real;flat out float v_placed;flat out float v_notUsed;void main() {gl_Position=projectTileWithElevation(a_anchor_pos,get_elevation(a_anchor_pos));gl_Position.xy=((a_box_real+0.5)*u_pixel_extrude_scale*2.0-1.0)*vec2(1.0,-1.0)*gl_Position.w;if (gl_Position.z/gl_Position.w < 1.1) {gl_Position.z=0.5;}v_placed=a_placed.x;v_notUsed=a_placed.y;}"), 9616 collisionCircle: prepare("flat in float v_radius;in vec2 v_extrude;flat in float v_collision;void main() {float alpha=0.5;float stroke_radius=0.9;float distance_to_center=length(v_extrude);float distance_to_edge=abs(distance_to_center-v_radius);float opacity_t=smoothstep(-stroke_radius,0.0,-distance_to_edge);vec4 color=mix(vec4(0.0,0.0,1.0,0.5),vec4(1.0,0.0,0.0,1.0),v_collision);fragColor=color*alpha*opacity_t;}", "layout(location=0) in vec2 a_pos;layout(location=1) in float a_radius;layout(location=2) in vec2 a_flags;flat out float v_radius;out vec2 v_extrude;flat out float v_collision;void main() {float radius=a_radius;float collision=a_flags.x;float vertexIdx=a_flags.y;vec2 quadVertexOffset=vec2(mix(-1.0,1.0,float(vertexIdx >=2.0)),mix(-1.0,1.0,float(vertexIdx >=1.0 && vertexIdx <=2.0)));vec2 quadVertexExtent=quadVertexOffset*radius;float padding_factor=1.2;v_radius=radius;v_extrude=quadVertexExtent*padding_factor;v_collision=collision;gl_Position=vec4((a_pos/u_viewport_size*2.0-1.0)*vec2(1.0,-1.0),0.0,1.0)+vec4(quadVertexExtent*padding_factor/u_viewport_size*2.0,0.0,0.0);}"), 9617 colorRelief: prepare("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform sampler2D u_image;uniform vec4 u_unpack;uniform sampler2D u_elevation_stops;uniform sampler2D u_color_stops;uniform int u_color_ramp_size;uniform float u_opacity;in vec2 v_pos;float getElevation(vec2 coord) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}float getElevationStop(int stop) {vec4 data=texelFetch(u_elevation_stops,ivec2(stop,0),0)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {float el=getElevation(v_pos);int r=(u_color_ramp_size-1);int l=0;float el_l=getElevationStop(l);float el_r=getElevationStop(r);while(r-l > 1){int m=(r+l)/2;float el_m=getElevationStop(m);if(el < el_m){r=m;el_r=el_m;}else\n{l=m;el_l=el_m;}}float x=(float(l)+(el-el_l)/(el_r-el_l)+0.5)/float(u_color_ramp_size);fragColor=u_opacity*texture(u_color_stops,vec2(x,0));\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);highp vec2 epsilon=2.0/u_dimension;float scale=(u_dimension.x-4.0)/u_dimension.x;v_pos=(a_pos/8192.0)*scale+epsilon;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}"), 9618 debug: prepare("uniform highp vec4 u_color;uniform sampler2D u_overlay;in vec2 v_uv;void main() {vec4 overlay_color=texture(u_overlay,v_uv);fragColor=mix(u_color,overlay_color,overlay_color.a);}", "layout(location=0) in vec2 a_pos;out vec2 v_uv;uniform float u_overlay_scale;void main() {v_uv=a_pos/8192.0;gl_Position=projectTileWithElevation(a_pos*u_overlay_scale,get_elevation(a_pos));}"), 9619 depth: prepare(clipping_mask_fragment_glsl_g_default, "layout(location=0) in vec2 a_pos;void main() {\n#ifdef GLOBE\ngl_Position=projectTileFor3D(a_pos,0.0);\n#else\ngl_Position=u_projection_matrix*vec4(a_pos,0.0,1.0);\n#endif\n}"), 9620 fill: prepare("#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float opacity\nfragColor=color*opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float opacity\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}gl_Position=projectTile(a_pos+u_fill_translate,a_pos);}"), 9621 fillOutline: prepare("in vec2 v_pos;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define highp vec4 outline_color\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 outline_color\n#pragma maplibre: initialize lowp float opacity\nfloat dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);fragColor=outline_color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define highp vec4 outline_color\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 outline_color\n#pragma maplibre: initialize lowp float opacity\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}gl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world_size;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n}"), 9622 fillOutlinePattern: prepare("uniform vec2 u_texsize;uniform sampler2D u_image;uniform float u_fade;uniform bool u_sdf_pattern;in vec2 v_pos_a;in vec2 v_pos_b;in vec2 v_pos;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\nvoid main() {\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);float dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);if (u_sdf_pattern) {highp float sdf_edge=(256.0-64.0)/256.0;highp float sdf_gamma_a=max(fwidth(color1.a)*0.5,1.0/255.0/16.0);highp float sdf_gamma_b=max(fwidth(color2.a)*0.5,1.0/255.0/16.0);float sdf_alpha_a=smoothstep(sdf_edge-sdf_gamma_a,sdf_edge+sdf_gamma_a,color1.a);float sdf_alpha_b=smoothstep(sdf_edge-sdf_gamma_b,sdf_edge+sdf_gamma_b,color2.a);fragColor=mix(color*sdf_alpha_a,color*sdf_alpha_b,u_fade)*alpha*opacity;} else {fragColor=mix(color1,color2,u_fade)*alpha*opacity;}\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;out vec2 v_pos;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\nvoid main() {\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world_size;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n}"), 9623 fillPattern: prepare("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform vec2 u_texsize;uniform float u_fade;uniform bool u_sdf_pattern;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\nvoid main() {\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);if (u_sdf_pattern) {highp float sdf_edge=(256.0-64.0)/256.0;highp float sdf_gamma_a=max(fwidth(color1.a)*0.5,1.0/255.0/16.0);highp float sdf_gamma_b=max(fwidth(color2.a)*0.5,1.0/255.0/16.0);float sdf_alpha_a=smoothstep(sdf_edge-sdf_gamma_a,sdf_edge+sdf_gamma_a,color1.a);float sdf_alpha_b=smoothstep(sdf_edge-sdf_gamma_b,sdf_edge+sdf_gamma_b,color2.a);fragColor=mix(color*sdf_alpha_a,color*sdf_alpha_b,u_fade)*opacity;} else {fragColor=mix(color1,color2,u_fade)*opacity;}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\nvoid main() {\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileZoomRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileZoomRatio,a_pos);}"), 9624 fillExtrusion: prepare("in vec4 v_color;void main() {fragColor=v_color;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;layout(location=1) in ivec4 a_normal_ed;\n#ifdef TERRAIN3D\nlayout(location=2) in vec2 a_centroid;\n#endif\nout vec4 v_color;\n#pragma maplibre: define highp float base\n#pragma maplibre: define highp float height\n#pragma maplibre: define highp vec4 color\nvoid main() {\n#pragma maplibre: initialize highp float base\n#pragma maplibre: initialize highp float height\n#pragma maplibre: initialize highp vec4 color\nvec3 normal=vec3(a_normal_ed.xyz);float gradient_base=max(0.0,base);float gradient_height=max(0.0,height);\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=float(a_normal_ed.x & 1);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate;\n#ifdef GLOBE\nvec3 spherePos=projectToSphere(posInTile,a_pos);gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation);\n#else\ngl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0);\n#endif\nfloat colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.0,0.0,0.0,1.0);vec4 ambientlight=vec4(0.03,0.03,0.03,1.0);color+=ambientlight;vec3 normalForLighting=normal/16384.0;float directional=clamp(dot(normalForLighting,u_lightpos),0.0,1.0);\n#ifdef GLOBE\nmat3 rotMatrix=globeGetRotationMatrix(spherePos);normalForLighting=rotMatrix*normalForLighting;directional=mix(directional,clamp(dot(normalForLighting,u_lightpos_globe),0.0,1.0),u_projection_transition);\n#endif\ndirectional=mix((1.0-u_lightintensity),max((1.0-colorvalue+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+gradient_base)*pow(gradient_height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.0,0.3,1.0-u_lightcolor.r),1.0);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.0,0.3,1.0-u_lightcolor.g),1.0);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.0,0.3,1.0-u_lightcolor.b),1.0);v_color*=u_opacity;}"), 9625 fillExtrusionPattern: prepare("uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;in vec4 v_lighting;\n#pragma maplibre: define lowp float base\n#pragma maplibre: define lowp float height\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\nvoid main() {\n#pragma maplibre: initialize lowp float base\n#pragma maplibre: initialize lowp float height\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);vec4 mixedColor=mix(color1,color2,u_fade);fragColor=mixedColor*v_lighting;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_height_factor;uniform vec3 u_scale;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;layout(location=0) in vec2 a_pos;layout(location=1) in ivec4 a_normal_ed;\n#ifdef TERRAIN3D\nlayout(location=2) in vec2 a_centroid;\n#endif\n#ifdef GLOBE\nout vec3 v_sphere_pos;\n#endif\nout vec2 v_pos_a;out vec2 v_pos_b;out vec4 v_lighting;\n#pragma maplibre: define lowp float base\n#pragma maplibre: define lowp float height\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\nvoid main() {\n#pragma maplibre: initialize lowp float base\n#pragma maplibre: initialize lowp float height\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec3 normal=vec3(a_normal_ed.xyz);float edgedistance=float(a_normal_ed.w);vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;float gradient_base=max(0.0,base);float gradient_height=max(0.0,height);\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=float(a_normal_ed.x & 1);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate;\n#ifdef GLOBE\nvec3 spherePos=projectToSphere(posInTile,a_pos);vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);v_sphere_pos=elevatedPos;gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation);\n#else\ngl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0);\n#endif\nvec2 pos=a_normal_ed.x==1 && a_normal_ed.y==0 && a_normal_ed.z==16384\n? a_pos\n: vec2(edgedistance,elevation*u_height_factor);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,pos);v_lighting=vec4(0.0,0.0,0.0,1.0);float directional=clamp(dot(normal/16383.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((0.5+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+gradient_base)*pow(gradient_height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_lighting.rgb+=clamp(directional*u_lightcolor,mix(vec3(0.0),vec3(0.3),1.0-u_lightcolor),vec3(1.0));v_lighting*=u_opacity;}"), 9626 hillshadePrepare: prepare("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_dimension;uniform float u_zoom;uniform vec4 u_unpack;float getElevation(ivec2 texel) {vec4 data=texelFetch(u_image,texel,0)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {ivec2 pos=ivec2(gl_FragCoord.xy)+ivec2(1);float tileSize=u_dimension.x-4.0;float a=getElevation(pos+ivec2(-1,-1));float b=getElevation(pos+ivec2(0,-1));float c=getElevation(pos+ivec2(1,-1));float d=getElevation(pos+ivec2(-1,0));float e=getElevation(pos);float f=getElevation(pos+ivec2(1,0));float g=getElevation(pos+ivec2(-1,1));float h=getElevation(pos+ivec2(0,1));float i=getElevation(pos+ivec2(1,1));float exaggerationFactor=u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3;float exaggeration=u_zoom < 15.0 ? (u_zoom-15.0)*exaggerationFactor : 0.0;vec2 deriv=vec2((c+f+f+i)-(a+d+d+g),(g+h+h+i)-(a+b+b+c))*tileSize/pow(2.0,exaggeration+(28.2562-u_zoom));fragColor=clamp(vec4(deriv.x/8.0+0.5,deriv.y/8.0+0.5,1.0,1.0),0.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform mat4 u_matrix;uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;layout(location=1) in vec2 a_texture_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_texture_pos/8192.0)*scale+epsilon;}"), 9627 hillshade: prepare("uniform sampler2D u_image;in highp vec2 v_pos;uniform vec2 u_latrange;uniform float u_exaggeration;uniform vec4 u_accent;uniform int u_method;uniform float u_altitudes[NUM_ILLUMINATION_SOURCES];uniform float u_azimuths[NUM_ILLUMINATION_SOURCES];uniform vec4 u_shadows[NUM_ILLUMINATION_SOURCES];uniform vec4 u_highlights[NUM_ILLUMINATION_SOURCES];\n#define PI 3.141592653589793\n#define STANDARD 0\n#define COMBINED 1\n#define IGOR 2\n#define MULTIDIRECTIONAL 3\n#define BASIC 4\nfloat get_aspect(vec2 deriv){return deriv.x !=0.0 ? atan(deriv.y,-deriv.x) : PI/2.0*(deriv.y > 0.0 ? 1.0 :-1.0);}void igor_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float aspect=get_aspect(deriv);float azimuth=u_azimuths[0]+PI;float slope_stength=atan(length(deriv))*2.0/PI;float aspect_strength=1.0-abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);float shadow_strength=slope_stength*aspect_strength;float highlight_strength=slope_stength*(1.0-aspect_strength);fragColor=u_shadows[0]*shadow_strength+u_highlights[0]*highlight_strength;}void standard_hillshade(vec2 deriv){float azimuth=u_azimuths[0]+PI;float slope=atan(0.625*length(deriv));float aspect=get_aspect(deriv);float intensity=u_exaggeration;float base=1.875-intensity*1.75;float maxValue=0.5*PI;float scaledSlope=intensity !=0.5 ? ((pow(base,slope)-1.0)/(pow(base,maxValue)-1.0))*maxValue : slope;float accent=cos(scaledSlope);vec4 accent_color=(1.0-accent)*u_accent*clamp(intensity*2.0,0.0,1.0);float shade=abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);vec4 shade_color=mix(u_shadows[0],u_highlights[0],shade)*sin(scaledSlope)*clamp(intensity*2.0,0.0,1.0);fragColor=accent_color*(1.0-shade_color.a)+shade_color;}void basic_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor=u_highlights[0]*(2.0*shade-1.0);}else\n{fragColor=u_shadows[0]*(1.0-2.0*shade);}}void multidirectional_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;fragColor=vec4(0,0,0,0);for(int i=0; i < NUM_ILLUMINATION_SOURCES; i++){float cos_alt=cos(u_altitudes[i]);float sin_alt=sin(u_altitudes[i]);float cos_az=-cos(u_azimuths[i]);float sin_az=-sin(u_azimuths[i]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor+=u_highlights[i]*(2.0*shade-1.0)/float(NUM_ILLUMINATION_SOURCES);}else\n{fragColor+=u_shadows[i]*(1.0-2.0*shade)/float(NUM_ILLUMINATION_SOURCES);}}}void combined_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=acos((sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv)));cang=clamp(cang,0.0,PI/2.0);float shade=cang*atan(length(deriv))*4.0/PI/PI;float highlight=(PI/2.0-cang)*atan(length(deriv))*4.0/PI/PI;fragColor=u_shadows[0]*shade+u_highlights[0]*highlight;}void main() {highp vec2 size=vec2(textureSize(u_image,0));highp vec2 texturePos=(v_pos*(size-2.0)+1.0)/size;vec4 pixel=texture(u_image,texturePos);float scaleFactor=cos(radians((u_latrange[0]-u_latrange[1])*(1.0-v_pos.y)+u_latrange[1]));vec2 deriv=((pixel.rg*8.0)-4.0)/scaleFactor;if (u_method==BASIC) {basic_hillshade(deriv);} else if (u_method==COMBINED) {combined_hillshade(deriv);} else if (u_method==IGOR) {igor_hillshade(deriv);} else if (u_method==MULTIDIRECTIONAL) {multidirectional_hillshade(deriv);} else if (u_method==STANDARD) {standard_hillshade(deriv);} else {standard_hillshade(deriv);}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform mat4 u_matrix;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);v_pos=a_pos/8192.0;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}"), 9628 line: prepare("flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\nclipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "\n#define scale 0.015873016\nlayout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp float v_linesofar;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float gapwidth\n#pragma maplibre: define lowp float offset\n#pragma maplibre: define mediump float width\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float gapwidth\n#pragma maplibre: initialize lowp float offset\n#pragma maplibre: initialize mediump float width\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);v_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*2.0;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"), 9629 lineGradient: prepare("uniform sampler2D u_image;flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;in highp vec2 v_uv;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\nclipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "\n#define scale 0.015873016\nlayout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform float u_image_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float gapwidth\n#pragma maplibre: define lowp float offset\n#pragma maplibre: define mediump float width\nvoid main() {\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float gapwidth\n#pragma maplibre: initialize lowp float offset\n#pragma maplibre: initialize mediump float width\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);highp float texel_height=1.0/u_image_height;highp float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"), 9630 linePattern: prepare("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform vec2 u_texsize;uniform float u_fade;uniform mediump vec3 u_scale;uniform sampler2D u_image;in vec2 v_normal;flat in vec2 v_width2;in float v_linesofar;in float v_gamma_scale;flat in float v_width;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\nclipAntimeridian();vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;vec2 pattern_size_a=vec2(display_size_a.x*fromScale/tileZoomRatio,display_size_a.y);vec2 pattern_size_b=vec2(display_size_b.x*toScale/tileZoomRatio,display_size_b.y);float aspect_a=display_size_a.y/v_width;float aspect_b=display_size_b.y/v_width;float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float x_a=mod(v_linesofar/pattern_size_a.x*aspect_a,1.0);float x_b=mod(v_linesofar/pattern_size_b.x*aspect_b,1.0);float y=0.5*v_normal.y+0.5;vec2 texel_size=1.0/u_texsize;vec2 pos_a=mix(pattern_tl_a*texel_size-texel_size,pattern_br_a*texel_size+texel_size,vec2(x_a,y));vec2 pos_b=mix(pattern_tl_b*texel_size-texel_size,pattern_br_b*texel_size+texel_size,vec2(x_b,y));vec4 color=mix(texture(u_image,pos_a),texture(u_image,pos_b),u_fade);fragColor=color*alpha*opacity;\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nlayout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_linesofar;out float v_gamma_scale;flat out float v_width;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define lowp float offset\n#pragma maplibre: define mediump float gapwidth\n#pragma maplibre: define mediump float width\n#pragma maplibre: define lowp float floorwidth\n#pragma maplibre: define lowp vec4 pattern_from\n#pragma maplibre: define lowp vec4 pattern_to\n#pragma maplibre: define lowp float pixel_ratio_from\n#pragma maplibre: define lowp float pixel_ratio_to\nvoid main() {\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize lowp float offset\n#pragma maplibre: initialize mediump float gapwidth\n#pragma maplibre: initialize mediump float width\n#pragma maplibre: initialize lowp float floorwidth\n#pragma maplibre: initialize mediump vec4 pattern_from\n#pragma maplibre: initialize mediump vec4 pattern_to\n#pragma maplibre: initialize lowp float pixel_ratio_from\n#pragma maplibre: initialize lowp float pixel_ratio_to\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_linesofar=a_linesofar;v_width2=vec2(outset,inset);v_width=floorwidth;}"), 9631 lineSDF: prepare("uniform lowp float u_lineatlas_width;uniform sampler2D u_image;uniform float u_mix;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float width\n#pragma maplibre: define lowp float floorwidth\n#pragma maplibre: define mediump vec4 dasharray_from\n#pragma maplibre: define mediump vec4 dasharray_to\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float width\n#pragma maplibre: initialize lowp float floorwidth\n#pragma maplibre: initialize mediump vec4 dasharray_from\n#pragma maplibre: initialize mediump vec4 dasharray_to\nclipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0/u_device_pixel_ratio)/min(dasharray_from.w,dasharray_to.w);alpha*=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nlayout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define highp vec4 color\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float gapwidth\n#pragma maplibre: define lowp float offset\n#pragma maplibre: define mediump float width\n#pragma maplibre: define lowp float floorwidth\n#pragma maplibre: define mediump vec4 dasharray_from\n#pragma maplibre: define mediump vec4 dasharray_to\nvoid main() {\n#pragma maplibre: initialize highp vec4 color\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float gapwidth\n#pragma maplibre: initialize lowp float offset\n#pragma maplibre: initialize mediump float width\n#pragma maplibre: initialize lowp float floorwidth\n#pragma maplibre: initialize mediump vec4 dasharray_from\n#pragma maplibre: initialize mediump vec4 dasharray_to\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nfloat u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}"), 9632 lineGradientSDF: prepare("uniform sampler2D u_image;uniform sampler2D u_image_dash;uniform float u_mix;uniform lowp float u_lineatlas_width;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;in highp vec2 v_uv;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float width\n#pragma maplibre: define lowp float floorwidth\n#pragma maplibre: define mediump vec4 dasharray_from\n#pragma maplibre: define mediump vec4 dasharray_to\nvoid main() {\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float width\n#pragma maplibre: initialize lowp float floorwidth\n#pragma maplibre: initialize mediump vec4 dasharray_from\n#pragma maplibre: initialize mediump vec4 dasharray_to\nclipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);float sdfdist_a=texture(u_image_dash,v_tex_a).a;float sdfdist_b=texture(u_image_dash,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0)/min(dasharray_from.w,dasharray_to.w);float dash_alpha=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*dash_alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nlayout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform float u_image_height;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv;out vec2 v_tex_a;out vec2 v_tex_b;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma maplibre: define lowp float blur\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define mediump float gapwidth\n#pragma maplibre: define lowp float offset\n#pragma maplibre: define mediump float width\n#pragma maplibre: define lowp float floorwidth\n#pragma maplibre: define mediump vec4 dasharray_from\n#pragma maplibre: define mediump vec4 dasharray_to\nvoid main() {\n#pragma maplibre: initialize lowp float blur\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize mediump float gapwidth\n#pragma maplibre: initialize lowp float offset\n#pragma maplibre: initialize mediump float width\n#pragma maplibre: initialize lowp float floorwidth\n#pragma maplibre: initialize mediump vec4 dasharray_from\n#pragma maplibre: initialize mediump vec4 dasharray_to\nif (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;float texel_height=1.0/u_image_height;float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nfloat u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}"), 9633 layerOpacity: prepare("uniform sampler2D u_image;uniform float u_opacity;in vec2 v_pos;void main() {fragColor=texture(u_image,v_pos)*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(0.0);\n#endif\n}", "layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=vec4(a_pos.x*2.0-1.0,1.0-a_pos.y*2.0,0.0,1.0);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}"), 9634 raster: prepare("uniform float u_fade_t;uniform float u_opacity;uniform sampler2D u_image0;uniform sampler2D u_image1;in vec3 v_pos0;in vec3 v_pos1;uniform float u_brightness_low;uniform float u_brightness_high;uniform float u_saturation_factor;uniform float u_contrast_factor;uniform vec3 u_spin_weights;void main() {vec4 color0=texture(u_image0,v_pos0.xy/v_pos0.z);vec4 color1=texture(u_image1,v_pos1.xy/v_pos1.z);if (color0.a > 0.0) {color0.rgb=color0.rgb/color0.a;}if (color1.a > 0.0) {color1.rgb=color1.rgb/color1.a;}vec4 color=mix(color0,color1,u_fade_t);color.a*=u_opacity;vec3 rgb=color.rgb;rgb=vec3(dot(rgb,u_spin_weights.xyz),dot(rgb,u_spin_weights.zxy),dot(rgb,u_spin_weights.yzx));float average=(color.r+color.g+color.b)/3.0;rgb+=(average-rgb)*u_saturation_factor;rgb=(rgb-0.5)*u_contrast_factor+0.5;vec3 u_high_vec=vec3(u_brightness_low,u_brightness_low,u_brightness_low);vec3 u_low_vec=vec3(u_brightness_high,u_brightness_high,u_brightness_high);fragColor=vec4(mix(u_high_vec,u_low_vec,rgb)*color.a,color.a);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "uniform vec2 u_tl_parent;uniform float u_scale_parent;uniform float u_buffer_scale;uniform vec3 u_image_warp;uniform vec4 u_coords_top;uniform vec4 u_coords_bottom;layout(location=0) in vec2 a_pos;out vec3 v_pos0;out vec3 v_pos1;void main() {vec2 fractionalPos=a_pos/8192.0;vec2 topLeft=u_coords_top.xy;vec2 topRight=u_coords_top.zw;vec2 bottomLeft=u_coords_bottom.xy;vec2 bottomRight=u_coords_bottom.zw;vec2 bilinearPos=mix(mix(topLeft,topRight,fractionalPos.x),mix(bottomLeft,bottomRight,fractionalPos.x),fractionalPos.y);float denominator=dot(u_image_warp.xy,fractionalPos)+1.0;vec2 acrossTop=topRight-topLeft+u_image_warp.x*topRight;vec2 downLeft=bottomLeft-topLeft+u_image_warp.y*bottomLeft;vec2 projectivePos=(acrossTop*fractionalPos.x+downLeft*fractionalPos.y+topLeft)/denominator;vec2 position=mix(projectivePos,bilinearPos,u_image_warp.z);gl_Position=projectTile(position,position);vec2 texturePos=((fractionalPos-0.5)/u_buffer_scale)+0.5;\n#ifdef GLOBE\nif (a_pos.y <-32767.5) {texturePos.y=0.0;}if (a_pos.y > 32766.5) {texturePos.y=1.0;}\n#endif\nfloat perspectiveRatio=mix(1.0/denominator,1.0,u_image_warp.z);v_pos0=vec3(texturePos*perspectiveRatio,perspectiveRatio);vec2 parentPos=(texturePos*u_scale_parent)+u_tl_parent;v_pos1=vec3(parentPos*perspectiveRatio,perspectiveRatio);}"), 9635 symbolIcon: prepare("uniform sampler2D u_texture;in vec2 v_tex;flat in float v_total_opacity;void main() {fragColor=texture(u_texture,v_tex)*v_total_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in uint a_fade_opacity;layout(location=5) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform bool u_rotate_symbol;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform vec2 u_texsize;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec2 v_tex;flat out float v_total_opacity;\n#pragma maplibre: define lowp float opacity\nvoid main() {\n#pragma maplibre: initialize lowp float opacity\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);vec2 a_pxoffset=a_pixeloffset.xy;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float fade_change=fade_opacity[1] > 0.5 ? u_symbol_fade_change :-u_symbol_fade_change;float visibility=calculate_visibility(projectedPoint);v_total_opacity=opacity*max(0.0,min(visibility,fade_opacity[0]+fade_change));if (v_total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset/16.0)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}gl_Position=finalPos;v_tex=a_tex/u_texsize;}"), 9636 symbolSDF: prepare("#define SDF_PX 8.0\nuniform bool u_is_halo;uniform bool u_is_plain;uniform sampler2D u_texture;uniform highp float u_gamma_scale;uniform bool u_is_text;in vec2 v_data0;in vec3 v_data1;\n#pragma maplibre: define highp vec4 fill_color\n#pragma maplibre: define highp vec4 halo_color\n#pragma maplibre: define lowp float halo_width\n#pragma maplibre: define lowp float halo_blur\nvoid main() {\n#pragma maplibre: initialize highp vec4 fill_color\n#pragma maplibre: initialize highp vec4 halo_color\n#pragma maplibre: initialize lowp float halo_width\n#pragma maplibre: initialize lowp float halo_blur\nfloat EDGE_GAMMA=0.105/u_device_pixel_ratio;vec2 tex=v_data0.xy;float gamma_scale=v_data1.x;float size=v_data1.y;float total_opacity=v_data1[2];float fontScale=u_is_text ? size/24.0 : size;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float inner_edge=(256.0-64.0)/256.0;lowp float dist=texture(u_texture,tex).a;lowp vec4 color_alpha_out_text,color_alpha_out_halo;if (u_is_plain){highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(inner_edge-gamma_scaled,inner_edge+gamma_scaled,dist);color_alpha_out_text=total_opacity*alpha*fill_color;}if (u_is_halo) {float gamma_halo=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);float inner_edge_halo=inner_edge+gamma_halo*gamma_scale;highp float gamma_scaled_halo=gamma_halo*gamma_scale;highp float alpha_halo=smoothstep(inner_edge_halo-gamma_scaled_halo,inner_edge_halo+gamma_scaled_halo,dist);highp float halo_edge=(6.0-halo_width/fontScale)/SDF_PX;alpha_halo= min(smoothstep(halo_edge-gamma_scaled_halo,halo_edge+gamma_scaled_halo,dist),1.0-alpha_halo);color_alpha_out_halo=total_opacity*alpha_halo*halo_color;}if (u_is_plain && u_is_halo) {fragColor=color_alpha_out_text+(1.-color_alpha_out_text.a)*color_alpha_out_halo;} else if (u_is_halo){fragColor=color_alpha_out_halo;} else {fragColor=color_alpha_out_text;}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in uint a_fade_opacity;layout(location=5) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform bool u_rotate_symbol;uniform vec2 u_texsize;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec2 v_data0;out vec3 v_data1;\n#pragma maplibre: define highp vec4 fill_color\n#pragma maplibre: define highp vec4 halo_color\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define lowp float halo_width\n#pragma maplibre: define lowp float halo_blur\nvoid main() {\n#pragma maplibre: initialize highp vec4 fill_color\n#pragma maplibre: initialize highp vec4 halo_color\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize lowp float halo_width\n#pragma maplibre: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);vec2 a_pxoffset=a_pixeloffset.xy/16.0;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_symbol_fade_change :-u_symbol_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0=a_tex/u_texsize;v_data1=vec3(gamma_scale,size,total_opacity);}"), 9637 symbolTextAndIcon: prepare("#define SDF_PX 8.0\n#define SDF 1.0\n#define ICON 0.0\nuniform bool u_is_halo;uniform bool u_is_text;uniform sampler2D u_texture;uniform sampler2D u_texture_icon;uniform highp float u_gamma_scale;in vec4 v_data0;in vec3 v_data1;flat in float v_is_sdf;\n#pragma maplibre: define highp vec4 fill_color\n#pragma maplibre: define highp vec4 halo_color\n#pragma maplibre: define lowp float halo_width\n#pragma maplibre: define lowp float halo_blur\nvoid main() {\n#pragma maplibre: initialize highp vec4 fill_color\n#pragma maplibre: initialize highp vec4 halo_color\n#pragma maplibre: initialize lowp float halo_width\n#pragma maplibre: initialize lowp float halo_blur\nfloat total_opacity=v_data1[2];if (v_is_sdf==ICON) {vec2 tex_icon=v_data0.zw;fragColor=texture(u_texture_icon,tex_icon)*total_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\nreturn;}vec2 tex=v_data0.xy;float EDGE_GAMMA=0.105/u_device_pixel_ratio;float gamma_scale=v_data1.x;float size=v_data1.y;float fontScale=size/24.0;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float buff=(256.0-64.0)/256.0;lowp float dist=texture(u_texture,tex).a;lowp vec4 color_alpha_out,color_alpha_out_halo;if (u_is_text) {highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(buff-gamma_scaled,buff+gamma_scaled,dist);color_alpha_out=fill_color*(alpha*total_opacity);}if (u_is_halo) {highp float gamma_halo=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);lowp float buff_halo=(6.0-halo_width/fontScale)/SDF_PX;highp float gamma_scaled_halo=gamma_halo*gamma_scale;highp float alpha_halo=smoothstep(buff_halo-gamma_scaled_halo,buff_halo+gamma_scaled_halo,dist);color_alpha_out_halo=halo_color*(alpha_halo*total_opacity);}if (u_is_text && u_is_halo) {fragColor=color_alpha_out+(1.-color_alpha_out.a)*color_alpha_out_halo;} else if (u_is_halo) {fragColor=color_alpha_out_halo;} else {fragColor=color_alpha_out;}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}", "layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec3 a_projected_pos;layout(location=3) in uint a_fade_opacity;layout(location=4) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_rotate_symbol;uniform vec2 u_texsize;uniform vec2 u_texsize_icon;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec4 v_data0;out vec3 v_data1;flat out float v_is_sdf;\n#pragma maplibre: define highp vec4 fill_color\n#pragma maplibre: define highp vec4 halo_color\n#pragma maplibre: define lowp float opacity\n#pragma maplibre: define lowp float halo_width\n#pragma maplibre: define lowp float halo_blur\nvoid main() {\n#pragma maplibre: initialize highp vec4 fill_color\n#pragma maplibre: initialize highp vec4 halo_color\n#pragma maplibre: initialize lowp float opacity\n#pragma maplibre: initialize lowp float halo_width\n#pragma maplibre: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);float is_sdf=float(a_data.z & 1u);float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_symbol_fade_change :-u_symbol_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=size/24.0;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map && !u_is_along_line) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0.xy=a_tex/u_texsize;v_data0.zw=a_tex/u_texsize_icon;v_data1=vec3(gamma_scale,size,total_opacity);v_is_sdf=is_sdf;}"), 9638 terrain: prepare("uniform sampler2D u_texture;uniform vec4 u_fog_color;uniform vec4 u_horizon_color;uniform float u_fog_ground_blend;uniform float u_fog_ground_blend_opacity;uniform float u_horizon_fog_blend;uniform bool u_is_globe_mode;in vec2 v_texture_pos;in float v_fog_depth;const float gamma=2.2;vec4 gammaToLinear(vec4 color) {return pow(color,vec4(gamma));}vec4 linearToGamma(vec4 color) {return pow(color,vec4(1.0/gamma));}void main() {vec4 surface_color=texture(u_texture,vec2(v_texture_pos.x,1.0-v_texture_pos.y));if (!u_is_globe_mode && u_fog_ground_blend_opacity > 0.0 && v_fog_depth > u_fog_ground_blend) {vec4 surface_color_linear=gammaToLinear(surface_color);float blend_color=smoothstep(0.0,1.0,max((v_fog_depth-u_horizon_fog_blend)/(1.0-u_horizon_fog_blend),0.0));vec4 fog_horizon_color_linear=mix(gammaToLinear(u_fog_color),gammaToLinear(u_horizon_color),blend_color);float factor_fog=max(v_fog_depth-u_fog_ground_blend,0.0)/(1.0-u_fog_ground_blend);fragColor=linearToGamma(mix(surface_color_linear,fog_horizon_color_linear,pow(factor_fog,2.0)*u_fog_ground_blend_opacity));} else {fragColor=surface_color;}}", "layout(location=0) in vec3 a_pos3d;uniform mat4 u_fog_matrix;uniform float u_ele_delta;out vec2 v_texture_pos;out float v_fog_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);vec4 pos=u_fog_matrix*vec4(a_pos3d.xy,ele,1.0);v_fog_depth=pos.z/pos.w*0.5+0.5;}"), 9639 terrainDepth: prepare("in float v_depth;const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitMsk=vec4(0.,vec3(1./256.0));highp vec4 pack(highp float value) {highp vec4 comp=fract(value*bitSh);comp-=comp.xxyz*bitMsk;return comp;}void main() {fragColor=pack(v_depth);}", "layout(location=0) in vec3 a_pos3d;uniform float u_ele_delta;out float v_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);v_depth=gl_Position.z/gl_Position.w;}"), 9640 atmosphere: prepare("#ifdef GL_ES\nprecision highp float;\n#endif\nin vec3 view_direction;uniform vec3 u_sun_pos;uniform vec3 u_globe_position;uniform float u_globe_radius;uniform float u_atmosphere_blend;/**Shader use from https:*Made some change to adapt to MapLibre Globe geometry*/const float PI=3.141592653589793;const int iSteps=5;const int jSteps=3;/*radius of the planet*/const float EARTH_RADIUS=6371e3;/*radius of the atmosphere*/const float ATMOS_RADIUS=6471e3;vec2 rsi(vec3 r0,vec3 rd,float sr) {float a=dot(rd,rd);float b=2.0*dot(rd,r0);float c=dot(r0,r0)-(sr*sr);float d=(b*b)-4.0*a*c;if (d < 0.0) return vec2(1e5,-1e5);return vec2((-b-sqrt(d))/(2.0*a),(-b+sqrt(d))/(2.0*a));}vec4 atmosphere(vec3 r,vec3 r0,vec3 pSun,float iSun,float rPlanet,float rAtmos,vec3 kRlh,float kMie,float shRlh,float shMie,float g) {pSun=normalize(pSun);r=normalize(r);vec2 p=rsi(r0,r,rAtmos);if (p.x > p.y) {return vec4(0.0,0.0,0.0,1.0);}if (p.x < 0.0) {p.x=0.0;}vec3 pos=r0+r*p.x;vec2 p2=rsi(r0,r,rPlanet);if (p2.x <=p2.y && p2.x > 0.0) {p.y=min(p.y,p2.x);}float iStepSize=(p.y-p.x)/float(iSteps);float iTime=p.x+iStepSize*0.5;vec3 totalRlh=vec3(0,0,0);vec3 totalMie=vec3(0,0,0);float iOdRlh=0.0;float iOdMie=0.0;float mu=dot(r,pSun);float mumu=mu*mu;float gg=g*g;float pRlh=3.0/(16.0*PI)*(1.0+mumu);float pMie=3.0/(8.0*PI)*((1.0-gg)*(mumu+1.0))/(pow(1.0+gg-2.0*mu*g,1.5)*(2.0+gg));for (int i=0; i < iSteps; i++) {vec3 iPos=r0+r*iTime;float iHeight=length(iPos)-rPlanet;float odStepRlh=exp(-iHeight/shRlh)*iStepSize;float odStepMie=exp(-iHeight/shMie)*iStepSize;iOdRlh+=odStepRlh;iOdMie+=odStepMie;float jStepSize=rsi(iPos,pSun,rAtmos).y/float(jSteps);float jTime=jStepSize*0.5;float jOdRlh=0.0;float jOdMie=0.0;for (int j=0; j < jSteps; j++) {vec3 jPos=iPos+pSun*jTime;float jHeight=length(jPos)-rPlanet;jOdRlh+=exp(-jHeight/shRlh)*jStepSize;jOdMie+=exp(-jHeight/shMie)*jStepSize;jTime+=jStepSize;}vec3 attn=exp(-(kMie*(iOdMie+jOdMie)+kRlh*(iOdRlh+jOdRlh)));totalRlh+=odStepRlh*attn;totalMie+=odStepMie*attn;iTime+=iStepSize;}float opacity=exp(-(length(kRlh)*length(totalRlh)+kMie*length(totalMie)));vec3 color=iSun*(pRlh*kRlh*totalRlh+pMie*kMie*totalMie);return vec4(color,opacity);}void main() {vec3 scale_camera_pos=-u_globe_position*EARTH_RADIUS/u_globe_radius;vec4 color=atmosphere(normalize(view_direction),scale_camera_pos,u_sun_pos,22.0,EARTH_RADIUS,ATMOS_RADIUS,vec3(5.5e-6,13.0e-6,22.4e-6),21e-6,8e3,1.2e3,0.758\n);color.rgb=1.0-exp(-1.0*color.rgb);color=pow(color,vec4(1.0/2.2));fragColor=vec4(color.rgb,1.0-color.a)*u_atmosphere_blend;}", "layout(location=0) in vec2 a_pos;uniform mat4 u_inv_proj_matrix;out vec3 view_direction;void main() {view_direction=(u_inv_proj_matrix*vec4(a_pos,0.0,1.0)).xyz;gl_Position=vec4(a_pos,0.0,1.0);}"), 9641 sky: prepare("uniform vec4 u_sky_color;uniform vec4 u_horizon_color;uniform vec2 u_horizon;uniform vec2 u_horizon_normal;uniform float u_sky_horizon_blend;uniform float u_sky_blend;uniform vec3 u_globe_position;uniform float u_globe_radius;uniform float u_atmosphere_blend;in vec3 v_view_direction;const float ATMOSPHERE_HEIGHT_TO_GLOBE_RADIUS=100000.0/6371000.0;void main() {float x=gl_FragCoord.x;float y=gl_FragCoord.y;float blend=(y-u_horizon.y)*u_horizon_normal.y+(x-u_horizon.x)*u_horizon_normal.x;float opacity=1.0;if (u_sky_blend > 0.0) {vec3 ray=normalize(v_view_direction);float globe_distance=length(u_globe_position);float angle_to_globe_center=acos(clamp(dot(ray,u_globe_position)/globe_distance,-1.0,1.0));float horizon_angle=asin(min(u_globe_radius/globe_distance,1.0));blend=mix(blend,((angle_to_globe_center-horizon_angle)*u_camera_to_center_distance+1.0)*u_device_pixel_ratio,u_sky_blend);float closest_approach=max(dot(ray,u_globe_position),0.0);float lowest_altitude=sqrt(max(globe_distance*globe_distance-closest_approach*closest_approach,0.0))-u_globe_radius;float atmosphere_height=u_globe_radius*ATMOSPHERE_HEIGHT_TO_GLOBE_RADIUS;float inside_atmosphere=1.0-smoothstep(atmosphere_height,10.0*atmosphere_height,globe_distance-u_globe_radius);float through_atmosphere=1.0-smoothstep(0.0,atmosphere_height,lowest_altitude);opacity=inside_atmosphere*through_atmosphere*(1.0-u_atmosphere_blend);}if (blend > 0.0) {if (blend < u_sky_horizon_blend) {fragColor=mix(u_sky_color,u_horizon_color,pow(1.0-blend/u_sky_horizon_blend,2.0));} else {fragColor=u_sky_color;}}fragColor*=mix(1.0,opacity,u_sky_blend);}", "layout(location=0) in vec2 a_pos;uniform mat4 u_inv_proj_matrix;out vec3 v_view_direction;void main() {v_view_direction=(u_inv_proj_matrix*vec4(a_pos,0.0,1.0)).xyz;gl_Position=vec4(a_pos,1.0,1.0);}") 9642}; 9643/** Expand #pragmas to #ifdefs, extract attributes and uniforms */ 9644function prepare(fragmentSource, vertexSource) { 9645 const re = /#pragma maplibre: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g; 9646 const vertexAttributes = vertexSource.match(/in ([\w]+) ([\w]+)/g); 9647 const fragmentUniforms = fragmentSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g); 9648 const vertexUniforms = vertexSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g); 9649 const shaderUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms; 9650 let locationCounter = vertexAttributes ? vertexAttributes.length : 0; 9651 const fragmentPragmas = {}; 9652 fragmentSource = fragmentSource.replace(re, (match, operation, precision, type, name) => { 9653 fragmentPragmas[name] = true; 9654 if (operation === "define") return ` 9655#ifndef HAS_UNIFORM_u_${name} 9656in ${precision} ${type} ${name}; 9657#else 9658uniform ${precision} ${type} u_${name}; 9659#endif 9660`; 9661 else return ` 9662#ifdef HAS_UNIFORM_u_${name} 9663 ${precision} ${type} ${name} = u_${name}; 9664#endif 9665`; 9666 }); 9667 vertexSource = vertexSource.replace(re, (match, operation, precision, type, name) => { 9668 const attrType = type === "float" ? "vec2" : "vec4"; 9669 const unpackType = name.match(/color/) ? "color" : attrType; 9670 if (fragmentPragmas[name]) { 9671 if (operation === "define") return ` 9672#ifndef HAS_UNIFORM_u_${name} 9673uniform lowp float u_${name}_t; 9674layout(location = ${locationCounter++}) in ${precision} ${attrType} a_${name}; 9675out ${precision} ${type} ${name}; 9676#else 9677uniform ${precision} ${type} u_${name}; 9678#endif 9679`; 9680 else if (unpackType === "vec4") return ` 9681#ifndef HAS_UNIFORM_u_${name} 9682 ${name} = a_${name}; 9683#else 9684 ${precision} ${type} ${name} = u_${name}; 9685#endif 9686`; 9687 else return ` 9688#ifndef HAS_UNIFORM_u_${name} 9689 ${name} = unpack_mix_${unpackType}(a_${name}, u_${name}_t); 9690#else 9691 ${precision} ${type} ${name} = u_${name}; 9692#endif 9693`; 9694 } else if (operation === "define") return ` 9695#ifndef HAS_UNIFORM_u_${name} 9696uniform lowp float u_${name}_t; 9697layout(location = ${locationCounter++}) in ${precision} ${attrType} a_${name}; 9698#else 9699uniform ${precision} ${type} u_${name}; 9700#endif 9701`; 9702 else if (unpackType === "vec4") return ` 9703#ifndef HAS_UNIFORM_u_${name} 9704 ${precision} ${type} ${name} = a_${name}; 9705#else 9706 ${precision} ${type} ${name} = u_${name}; 9707#endif 9708`; 9709 else return ` 9710#ifndef HAS_UNIFORM_u_${name} 9711 ${precision} ${type} ${name} = unpack_mix_${unpackType}(a_${name}, u_${name}_t); 9712#else 9713 ${precision} ${type} ${name} = u_${name}; 9714#endif 9715`; 9716 }); 9717 return { 9718 fragmentSource, 9719 vertexSource, 9720 staticAttributes: vertexAttributes, 9721 staticUniforms: shaderUniforms 9722 }; 9723} 9724//#endregion 9725//#region src/geo/projection/mercator_projection.ts 9726const MercatorShaderDefine = "#define PROJECTION_MERCATOR"; 9727const MercatorShaderVariantKey = "mercator"; 9728var MercatorProjection = class { 9729 constructor() { 9730 this._cachedMesh = null; 9731 } 9732 get name() { 9733 return "mercator"; 9734 } 9735 get useSubdivision() { 9736 return false; 9737 } 9738 get shaderVariantName() { 9739 return MercatorShaderVariantKey; 9740 } 9741 get shaderDefine() { 9742 return MercatorShaderDefine; 9743 } 9744 get shaderPreludeCode() { 9745 return shaders.projectionMercator; 9746 } 9747 get vertexShaderPreludeCode() { 9748 return shaders.projectionMercator.vertexSource; 9749 } 9750 get subdivisionGranularity() { 9751 return SubdivisionGranularitySetting.noSubdivision; 9752 } 9753 get useGlobeControls() { 9754 return false; 9755 } 9756 get transitionState() { 9757 return 0; 9758 } 9759 destroy() {} 9760 getMeshFromTileID(context, _tileID, _hasBorder, _allowPoles, _usage) { 9761 if (this._cachedMesh) return this._cachedMesh; 9762 const tileExtentArray = new PosArray(); 9763 tileExtentArray.emplaceBack(0, 0); 9764 tileExtentArray.emplaceBack(EXTENT, 0); 9765 tileExtentArray.emplaceBack(0, EXTENT); 9766 tileExtentArray.emplaceBack(EXTENT, EXTENT); 9767 const tileExtentBuffer = context.createVertexBuffer(tileExtentArray, posAttributes.members); 9768 const tileExtentSegments = SegmentVector.simpleSegment(0, 0, 4, 2); 9769 const quadTriangleIndices = new TriangleIndexArray(); 9770 quadTriangleIndices.emplaceBack(1, 0, 2); 9771 quadTriangleIndices.emplaceBack(1, 2, 3); 9772 const quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices); 9773 this._cachedMesh = new Mesh(tileExtentBuffer, quadTriangleIndexBuffer, tileExtentSegments); 9774 return this._cachedMesh; 9775 } 9776 recalculate() {} 9777 hasTransition() { 9778 return false; 9779 } 9780}; 9781//#endregion 9782//#region src/geo/edge_insets.ts 9783/** 9784* An `EdgeInset` object represents screen space padding applied to the edges of the viewport. 9785* This shifts the apparent center or the vanishing point of the map. This is useful for adding floating UI elements 9786* on top of the map and having the vanishing point shift as UI elements resize. 9787* 9788* @group Geography and Geometry 9789*/ 9790var EdgeInsets = class EdgeInsets { 9791 constructor(top = 0, bottom = 0, left = 0, right = 0) { 9792 if (isNaN(top) || top < 0 || isNaN(bottom) || bottom < 0 || isNaN(left) || left < 0 || isNaN(right) || right < 0) throw new Error("Invalid value for edge-insets, top, bottom, left and right must all be numbers"); 9793 this.top = top; 9794 this.bottom = bottom; 9795 this.left = left; 9796 this.right = right; 9797 } 9798 /** 9799 * Interpolates the inset in-place. 9800 * This maintains the current inset value for any inset not present in `target`. 9801 * @param start - interpolation start 9802 * @param target - interpolation target 9803 * @param t - interpolation step/weight 9804 * @returns the insets 9805 */ 9806 interpolate(start, target, t) { 9807 if (target.top != null && start.top != null) this.top = interpolateFactory.number(start.top, target.top, t); 9808 if (target.bottom != null && start.bottom != null) this.bottom = interpolateFactory.number(start.bottom, target.bottom, t); 9809 if (target.left != null && start.left != null) this.left = interpolateFactory.number(start.left, target.left, t); 9810 if (target.right != null && start.right != null) this.right = interpolateFactory.number(start.right, target.right, t); 9811 return this; 9812 } 9813 /** 9814 * Utility method that computes the new apparent center or vanishing point after applying insets. 9815 * This is in pixels and with the top left being (0.0) and +y being downwards. 9816 * 9817 * @param width - the width 9818 * @param height - the height 9819 * @returns the point 9820 */ 9821 getCenter(width, height) { 9822 const x = clamp((this.left + width - this.right) / 2, 0, width); 9823 const y = clamp((this.top + height - this.bottom) / 2, 0, height); 9824 return new Point(x, y); 9825 } 9826 equals(other) { 9827 return this.top === other.top && this.bottom === other.bottom && this.left === other.left && this.right === other.right; 9828 } 9829 clone() { 9830 return new EdgeInsets(this.top, this.bottom, this.left, this.right); 9831 } 9832 /** 9833 * Returns the current state as json, useful when you want to have a 9834 * read-only representation of the inset. 9835 * 9836 * @returns state as json 9837 */ 9838 toJSON() { 9839 return { 9840 top: this.top, 9841 bottom: this.bottom, 9842 left: this.left, 9843 right: this.right 9844 }; 9845 } 9846}; 9847//#endregion 9848//#region src/geo/transform_helper.ts 9849/** 9850* If a path crossing the antimeridian would be shorter, extend the final coordinate so that 9851* interpolating between the two endpoints will cross it. 9852* @param center - The LngLat object of the desired center. This object will be mutated. 9853*/ 9854function normalizeCenter(tr, center) { 9855 if (!tr.renderWorldCopies || tr.lngRange) return; 9856 const delta = center.lng - tr.center.lng; 9857 center.lng += delta > 180 ? -360 : delta < -180 ? 360 : 0; 9858} 9859function getTileZoom(zoom) { 9860 return Math.max(0, Math.floor(zoom)); 9861} 9862/** 9863* @internal 9864* This class stores all values that define a transform's state, 9865* such as center, zoom, minZoom, etc. 9866* This can be used as a helper for implementing the ITransform interface. 9867*/ 9868var TransformHelper = class { 9869 constructor(callbacks, options) { 9870 this.applyConstrain = (lngLat, zoom) => { 9871 if (this._constrainOverride !== null) return this._constrainOverride(lngLat, zoom); 9872 else return this._callbacks.defaultConstrain(lngLat, zoom); 9873 }; 9874 this._callbacks = callbacks; 9875 this._tileSize = 512; 9876 this._renderWorldCopies = options?.renderWorldCopies === void 0 ? true : !!options?.renderWorldCopies; 9877 this._minZoom = options?.minZoom || 0; 9878 this._maxZoom = options?.maxZoom || 22; 9879 this._minPitch = options?.minPitch === void 0 || options?.minPitch === null ? 0 : options?.minPitch; 9880 this._maxPitch = options?.maxPitch === void 0 || options?.maxPitch === null ? 60 : options?.maxPitch; 9881 this._constrainOverride = options?.constrainOverride ?? null; 9882 this.setMaxBounds(); 9883 this._width = 0; 9884 this._height = 0; 9885 this._center = new LngLat(0, 0); 9886 this._elevation = 0; 9887 this._zoom = 0; 9888 this._tileZoom = getTileZoom(this._zoom); 9889 this._scale = zoomScale(this._zoom); 9890 this._bearingInRadians = 0; 9891 this._fovInRadians = .6435011087932844; 9892 this._pitchInRadians = 0; 9893 this._rollInRadians = 0; 9894 this._unmodified = true; 9895 this._edgeInsets = new EdgeInsets(); 9896 this._minElevationForCurrentTile = 0; 9897 this._autoCalculateNearFarZ = true; 9898 } 9899 apply(thatI, constrain) { 9900 this._constrainOverride = thatI.constrainOverride; 9901 this._latRange = thatI.latRange; 9902 this._lngRange = thatI.lngRange; 9903 this._width = thatI.width; 9904 this._height = thatI.height; 9905 this._center = thatI.center; 9906 this._elevation = thatI.elevation; 9907 this._minElevationForCurrentTile = thatI.minElevationForCurrentTile; 9908 this._zoom = thatI.zoom; 9909 this._tileZoom = getTileZoom(this._zoom); 9910 this._scale = zoomScale(this._zoom); 9911 this._bearingInRadians = thatI.bearingInRadians; 9912 this._fovInRadians = thatI.fovInRadians; 9913 this._pitchInRadians = thatI.pitchInRadians; 9914 this._rollInRadians = thatI.rollInRadians; 9915 this._unmodified = thatI.unmodified; 9916 this._edgeInsets = new EdgeInsets(thatI.padding.top, thatI.padding.bottom, thatI.padding.left, thatI.padding.right); 9917 this._minZoom = thatI.minZoom; 9918 this._maxZoom = thatI.maxZoom; 9919 this._minPitch = thatI.minPitch; 9920 this._maxPitch = thatI.maxPitch; 9921 this._renderWorldCopies = thatI.renderWorldCopies; 9922 this._cameraToCenterDistance = thatI.cameraToCenterDistance; 9923 this._nearZ = thatI.nearZ; 9924 this._farZ = thatI.farZ; 9925 this._autoCalculateNearFarZ = thatI.autoCalculateNearFarZ; 9926 if (constrain) this.constrainInternal(); 9927 this._calcMatrices(); 9928 } 9929 get pixelsToClipSpaceMatrix() { 9930 return this._pixelsToClipSpaceMatrix; 9931 } 9932 get clipSpaceToPixelsMatrix() { 9933 return this._clipSpaceToPixelsMatrix; 9934 } 9935 get minElevationForCurrentTile() { 9936 return this._minElevationForCurrentTile; 9937 } 9938 setMinElevationForCurrentTile(ele) { 9939 this._minElevationForCurrentTile = ele; 9940 } 9941 get tileSize() { 9942 return this._tileSize; 9943 } 9944 get tileZoom() { 9945 return this._tileZoom; 9946 } 9947 get scale() { 9948 return this._scale; 9949 } 9950 /** 9951 * Gets the transform's width in pixels. Use {@link resize} to set the transform's size. 9952 */ 9953 get width() { 9954 return this._width; 9955 } 9956 /** 9957 * Gets the transform's height in pixels. Use {@link resize} to set the transform's size. 9958 */ 9959 get height() { 9960 return this._height; 9961 } 9962 /** 9963 * Gets the transform's bearing in radians. 9964 */ 9965 get bearingInRadians() { 9966 return this._bearingInRadians; 9967 } 9968 get lngRange() { 9969 return this._lngRange; 9970 } 9971 get latRange() { 9972 return this._latRange; 9973 } 9974 get pixelsToGLUnits() { 9975 return this._pixelsToGLUnits; 9976 } 9977 get minZoom() { 9978 return this._minZoom; 9979 } 9980 setMinZoom(zoom) { 9981 if (this._minZoom === zoom) return; 9982 this._minZoom = zoom; 9983 const unmodified = this._unmodified; 9984 this.setZoom(this.applyConstrain(this._center, this.zoom).zoom); 9985 this._unmodified = unmodified; 9986 } 9987 get maxZoom() { 9988 return this._maxZoom; 9989 } 9990 setMaxZoom(zoom) { 9991 if (this._maxZoom === zoom) return; 9992 this._maxZoom = zoom; 9993 const unmodified = this._unmodified; 9994 this.setZoom(this.applyConstrain(this._center, this.zoom).zoom); 9995 this._unmodified = unmodified; 9996 } 9997 get minPitch() { 9998 return this._minPitch; 9999 } 10000 setMinPitch(pitch) { 10001 if (this._minPitch === pitch) return; 10002 this._minPitch = pitch; 10003 const unmodified = this._unmodified; 10004 this.setPitch(Math.max(this.pitch, pitch)); 10005 this._unmodified = unmodified; 10006 } 10007 get maxPitch() { 10008 return this._maxPitch; 10009 } 10010 setMaxPitch(pitch) { 10011 if (this._maxPitch === pitch) return; 10012 this._maxPitch = pitch; 10013 const unmodified = this._unmodified; 10014 this.setPitch(Math.min(this.pitch, pitch)); 10015 this._unmodified = unmodified; 10016 } 10017 get renderWorldCopies() { 10018 return this._renderWorldCopies; 10019 } 10020 setRenderWorldCopies(renderWorldCopies) { 10021 if (renderWorldCopies === void 0) renderWorldCopies = true; 10022 else if (renderWorldCopies === null) renderWorldCopies = false; 10023 this._renderWorldCopies = renderWorldCopies; 10024 } 10025 get constrainOverride() { 10026 return this._constrainOverride; 10027 } 10028 setConstrainOverride(constrain) { 10029 if (constrain === void 0) constrain = null; 10030 if (this._constrainOverride === constrain) return; 10031 this._constrainOverride = constrain; 10032 this.constrainInternal(); 10033 this._calcMatrices(); 10034 } 10035 get worldSize() { 10036 return this._tileSize * this._scale; 10037 } 10038 get centerOffset() { 10039 return this.centerPoint._sub(this.size._div(2)); 10040 } 10041 /** 10042 * Gets the transform's dimensions packed into a Point object. 10043 */ 10044 get size() { 10045 return new Point(this._width, this._height); 10046 } 10047 get bearing() { 10048 return this._bearingInRadians / Math.PI * 180; 10049 } 10050 setBearing(bearing) { 10051 const b = wrap(bearing, -180, 180) * Math.PI / 180; 10052 if (this._bearingInRadians === b) return; 10053 this._unmodified = false; 10054 this._bearingInRadians = b; 10055 this._calcMatrices(); 10056 this._rotationMatrix = create$2(); 10057 rotate(this._rotationMatrix, this._rotationMatrix, -this._bearingInRadians); 10058 } 10059 get rotationMatrix() { 10060 return this._rotationMatrix; 10061 } 10062 get pitchInRadians() { 10063 return this._pitchInRadians; 10064 } 10065 get pitch() { 10066 return this._pitchInRadians / Math.PI * 180; 10067 } 10068 setPitch(pitch) { 10069 const p = clamp(pitch, this.minPitch, this.maxPitch) / 180 * Math.PI; 10070 if (this._pitchInRadians === p) return; 10071 this._unmodified = false; 10072 this._pitchInRadians = p; 10073 this._calcMatrices(); 10074 } 10075 get rollInRadians() { 10076 return this._rollInRadians; 10077 } 10078 get roll() { 10079 return this._rollInRadians / Math.PI * 180; 10080 } 10081 setRoll(roll) { 10082 const r = roll / 180 * Math.PI; 10083 if (this._rollInRadians === r) return; 10084 this._unmodified = false; 10085 this._rollInRadians = r; 10086 this._calcMatrices(); 10087 } 10088 get fovInRadians() { 10089 return this._fovInRadians; 10090 } 10091 get fov() { 10092 return radiansToDegrees(this._fovInRadians); 10093 } 10094 setFov(fov) { 10095 fov = clamp(fov, .1, 150); 10096 if (this.fov === fov) return; 10097 this._unmodified = false; 10098 this._fovInRadians = degreesToRadians(fov); 10099 this._calcMatrices(); 10100 } 10101 get zoom() { 10102 return this._zoom; 10103 } 10104 setZoom(zoom) { 10105 const constrainedZoom = this.applyConstrain(this._center, zoom).zoom; 10106 if (this._zoom === constrainedZoom) return; 10107 this._unmodified = false; 10108 this._zoom = constrainedZoom; 10109 this._tileZoom = Math.max(0, Math.floor(constrainedZoom)); 10110 this._scale = zoomScale(constrainedZoom); 10111 this.constrainInternal(); 10112 this._calcMatrices(); 10113 } 10114 get center() { 10115 return this._center; 10116 } 10117 setCenter(center) { 10118 if (center.lat === this._center.lat && center.lng === this._center.lng) return; 10119 this._unmodified = false; 10120 this._center = center; 10121 this.constrainInternal(); 10122 this._calcMatrices(); 10123 } 10124 /** 10125 * Elevation at current center point, meters above sea level 10126 */ 10127 get elevation() { 10128 return this._elevation; 10129 } 10130 setElevation(elevation) { 10131 if (elevation === this._elevation) return; 10132 this._elevation = elevation; 10133 this.constrainInternal(); 10134 this._calcMatrices(); 10135 } 10136 get padding() { 10137 return this._edgeInsets.toJSON(); 10138 } 10139 setPadding(padding) { 10140 if (this._edgeInsets.equals(padding)) return; 10141 this._unmodified = false; 10142 this._edgeInsets.interpolate(this._edgeInsets, padding, 1); 10143 this._calcMatrices(); 10144 } 10145 /** 10146 * The center of the screen in pixels with the top-left corner being (0,0) 10147 * and +y axis pointing downwards. This accounts for padding. 10148 */ 10149 get centerPoint() { 10150 return this._edgeInsets.getCenter(this._width, this._height); 10151 } 10152 /** 10153 * @internal 10154 */ 10155 get pixelsPerMeter() { 10156 return this._pixelPerMeter; 10157 } 10158 get unmodified() { 10159 return this._unmodified; 10160 } 10161 get cameraToCenterDistance() { 10162 return this._cameraToCenterDistance; 10163 } 10164 get nearZ() { 10165 return this._nearZ; 10166 } 10167 get farZ() { 10168 return this._farZ; 10169 } 10170 get autoCalculateNearFarZ() { 10171 return this._autoCalculateNearFarZ; 10172 } 10173 overrideNearFarZ(nearZ, farZ) { 10174 this._autoCalculateNearFarZ = false; 10175 this._nearZ = nearZ; 10176 this._farZ = farZ; 10177 this._calcMatrices(); 10178 } 10179 clearNearFarZOverride() { 10180 this._autoCalculateNearFarZ = true; 10181 this._calcMatrices(); 10182 } 10183 /** 10184 * Returns if the padding params match 10185 * 10186 * @param padding - the padding to check against 10187 * @returns true if they are equal, false otherwise 10188 */ 10189 isPaddingEqual(padding) { 10190 return this._edgeInsets.equals(padding); 10191 } 10192 /** 10193 * Helper method to update edge-insets in place 10194 * 10195 * @param start - the starting padding 10196 * @param target - the target padding 10197 * @param t - the step/weight 10198 */ 10199 interpolatePadding(start, target, t) { 10200 this._unmodified = false; 10201 this._edgeInsets.interpolate(start, target, t); 10202 this.constrainInternal(); 10203 this._calcMatrices(); 10204 } 10205 resize(width, height, constrain = true) { 10206 this._width = width; 10207 this._height = height; 10208 if (constrain) this.constrainInternal(); 10209 this._calcMatrices(); 10210 } 10211 /** 10212 * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. 10213 * @returns max bounds 10214 */ 10215 getMaxBounds() { 10216 if (this._latRange?.length !== 2 || this._lngRange?.length !== 2) return null; 10217 return new LngLatBounds([this._lngRange[0], this._latRange[0]], [this._lngRange[1], this._latRange[1]]); 10218 } 10219 /** 10220 * Sets or clears the map's geographical constraints. 10221 * @param bounds - A {@link LngLatBounds} object describing the new geographic boundaries of the map. 10222 */ 10223 setMaxBounds(bounds) { 10224 if (bounds) { 10225 this._lngRange = [bounds.getWest(), bounds.getEast()]; 10226 this._latRange = [bounds.getSouth(), bounds.getNorth()]; 10227 this.constrainInternal(); 10228 } else { 10229 this._lngRange = null; 10230 this._latRange = [-MAX_VALID_LATITUDE, MAX_VALID_LATITUDE]; 10231 } 10232 } 10233 /** 10234 * When the map is pitched, some of the 3D features that intersect a query will not intersect 10235 * the query at the surface of the earth. Instead the feature may be closer and only intersect 10236 * the query because it extrudes into the air. 10237 * @param queryGeometry - For point queries, the line from the query point to the "camera point", 10238 * for other geometries, the envelope of the query geometry and the "camera point" 10239 * @returns a geometry that includes all of the original query as well as all possible ares of the 10240 * screen where the *base* of a visible extrusion could be. 10241 * 10242 */ 10243 getCameraQueryGeometry(cameraPoint, queryGeometry) { 10244 if (queryGeometry.length === 1) return [queryGeometry[0], cameraPoint]; 10245 else { 10246 const { minX, minY, maxX, maxY } = Bounds.fromPoints(queryGeometry).extend(cameraPoint); 10247 return [ 10248 new Point(minX, minY), 10249 new Point(maxX, minY), 10250 new Point(maxX, maxY), 10251 new Point(minX, maxY), 10252 new Point(minX, minY) 10253 ]; 10254 } 10255 } 10256 /** 10257 * @internal 10258 * Snaps the transform's center, zoom, etc. into the valid range. 10259 */ 10260 constrainInternal() { 10261 if (!this.center || !this._width || !this._height || this._constraining) return; 10262 this._constraining = true; 10263 const unmodified = this._unmodified; 10264 const { center, zoom } = this.applyConstrain(this.center, this.zoom); 10265 this.setCenter(center); 10266 this.setZoom(zoom); 10267 this._unmodified = unmodified; 10268 this._constraining = false; 10269 } 10270 /** 10271 * This function is called every time one of the transform's defining properties (center, pitch, etc.) changes. 10272 * This function should update the transform's internal data, such as matrices. 10273 * Any derived `_calcMatrices` function should also call the base function first. The base function only depends on the `_width` and `_height` fields. 10274 * While either dimension is zero there is no view to build, so the derived function is not called at all. 10275 */ 10276 _calcMatrices() { 10277 this._pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; 10278 if (!this._width || !this._height) return; 10279 this._pixelsToGLUnits = [2 / this._width, -2 / this._height]; 10280 let m = identity(/* @__PURE__ */ new Float64Array(16)); 10281 scale(m, m, [ 10282 this._width / 2, 10283 -this._height / 2, 10284 1 10285 ]); 10286 translate(m, m, [ 10287 1, 10288 -1, 10289 0 10290 ]); 10291 this._clipSpaceToPixelsMatrix = m; 10292 m = identity(/* @__PURE__ */ new Float64Array(16)); 10293 scale(m, m, [ 10294 1, 10295 -1, 10296 1 10297 ]); 10298 translate(m, m, [ 10299 -1, 10300 -1, 10301 0 10302 ]); 10303 scale(m, m, [ 10304 2 / this._width, 10305 2 / this._height, 10306 1 10307 ]); 10308 this._pixelsToClipSpaceMatrix = m; 10309 const halfFov = this.fovInRadians / 2; 10310 this._cameraToCenterDistance = .5 / Math.tan(halfFov) * this._height; 10311 this._callbacks.calcMatrices(); 10312 } 10313 calculateCenterFromCameraLngLatAlt(lnglat, alt, bearing, pitch) { 10314 const cameraBearing = bearing !== void 0 ? bearing : this.bearing; 10315 const cameraPitch = pitch = pitch !== void 0 ? pitch : this.pitch; 10316 const { distanceToCenter, clampedElevation } = this._distanceToCenterFromAltElevationPitch(alt, this.elevation, cameraPitch); 10317 const { x, y } = cameraDirectionFromPitchBearing(cameraPitch, cameraBearing); 10318 const camMercator = MercatorCoordinate.fromLngLat(lnglat, alt); 10319 let metersPerMercUnit = altitudeFromMercatorZ(1, camMercator.y); 10320 let centerMercator; 10321 let dMercator; 10322 let iter = 0; 10323 const maxIter = 10; 10324 do { 10325 iter += 1; 10326 if (iter > maxIter) break; 10327 dMercator = distanceToCenter / metersPerMercUnit; 10328 const dx = x * dMercator; 10329 const dy = y * dMercator; 10330 centerMercator = new MercatorCoordinate(camMercator.x + dx, camMercator.y + dy); 10331 metersPerMercUnit = 1 / centerMercator.meterInMercatorCoordinateUnits(); 10332 } while (Math.abs(distanceToCenter - dMercator * metersPerMercUnit) > 1e-12); 10333 return { 10334 center: centerMercator.toLngLat(), 10335 elevation: clampedElevation, 10336 zoom: scaleZoom(this.height / 2 / Math.tan(this.fovInRadians / 2) / dMercator / this.tileSize) 10337 }; 10338 } 10339 recalculateZoomAndCenter(elevation) { 10340 if (this.elevation - elevation === 0) return; 10341 const mercUnitsPerPixel = 1 / this.worldSize; 10342 const originalPixelsPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; 10343 const originalCenterMercator = MercatorCoordinate.fromLngLat(this.center, this.elevation); 10344 const originalCenterPixelX = originalCenterMercator.x / mercUnitsPerPixel; 10345 const originalCenterPixelY = originalCenterMercator.y / mercUnitsPerPixel; 10346 const originalCenterPixelZ = originalCenterMercator.z / mercUnitsPerPixel; 10347 const cameraPitch = this.pitch; 10348 const cameraBearing = this.bearing; 10349 const { x, y, z } = cameraDirectionFromPitchBearing(cameraPitch, cameraBearing); 10350 const dCamPixel = this.cameraToCenterDistance; 10351 const camPixelX = originalCenterPixelX + dCamPixel * -x; 10352 const camPixelY = originalCenterPixelY + dCamPixel * -y; 10353 const camPixelZ = originalCenterPixelZ + dCamPixel * z; 10354 const { distanceToCenter, clampedElevation } = this._distanceToCenterFromAltElevationPitch(camPixelZ / originalPixelsPerMeter, elevation, cameraPitch); 10355 const distanceToCenterPixels = distanceToCenter * originalPixelsPerMeter; 10356 const centerPixelX = camPixelX + x * distanceToCenterPixels; 10357 const centerPixelY = camPixelY + y * distanceToCenterPixels; 10358 const center = new MercatorCoordinate(centerPixelX * mercUnitsPerPixel, centerPixelY * mercUnitsPerPixel, 0).toLngLat(); 10359 const mercUnitsPerMeter = mercatorZfromAltitude(1, center.lat); 10360 const zoom = scaleZoom(this.height / 2 / Math.tan(this.fovInRadians / 2) / distanceToCenter / mercUnitsPerMeter / this.tileSize); 10361 this._elevation = clampedElevation; 10362 this._center = center; 10363 this.setZoom(zoom); 10364 } 10365 _distanceToCenterFromAltElevationPitch(alt, elevation, pitch) { 10366 const dzNormalized = -Math.cos(degreesToRadians(pitch)); 10367 const altitudeAGL = alt - elevation; 10368 let distanceToCenter; 10369 let clampedElevation = elevation; 10370 if (dzNormalized * altitudeAGL >= 0 || Math.abs(dzNormalized) < .1) { 10371 distanceToCenter = 1e4; 10372 clampedElevation = alt + distanceToCenter * dzNormalized; 10373 } else distanceToCenter = -altitudeAGL / dzNormalized; 10374 return { 10375 distanceToCenter, 10376 clampedElevation 10377 }; 10378 } 10379 getCameraPoint() { 10380 const pitch = this.pitchInRadians; 10381 const offset = Math.tan(pitch) * (this.cameraToCenterDistance || 1); 10382 return this.centerPoint.add(new Point(offset * Math.sin(this.rollInRadians), offset * Math.cos(this.rollInRadians))); 10383 } 10384 getMercatorTileCoordinates(overscaledTileID) { 10385 if (!overscaledTileID) return [ 10386 0, 10387 0, 10388 1, 10389 1 10390 ]; 10391 const scale = overscaledTileID.canonical.z >= 0 ? 1 << overscaledTileID.canonical.z : Math.pow(2, overscaledTileID.canonical.z); 10392 return [ 10393 overscaledTileID.canonical.x / scale, 10394 overscaledTileID.canonical.y / scale, 10395 1 / scale / EXTENT, 10396 1 / scale / EXTENT 10397 ]; 10398 } 10399}; 10400//#endregion 10401//#region src/geo/projection/mercator_covering_tiles_details_provider.ts 10402var MercatorCoveringTilesDetailsProvider = class { 10403 distanceToTile2d(pointX, pointY, _tileID, boundingVolume) { 10404 const aabb = boundingVolume; 10405 const distanceX = aabb.distanceX([pointX, pointY]); 10406 const distanceY = aabb.distanceY([pointX, pointY]); 10407 return Math.hypot(distanceX, distanceY); 10408 } 10409 /** 10410 * Returns the wrap value for a given tile, computed so that tiles will remain loaded when crossing the antimeridian. 10411 */ 10412 getWrap(centerCoord, tileID, parentWrap) { 10413 return parentWrap; 10414 } 10415 /** 10416 * Returns the AABB of the specified tile. 10417 * @param tileID - Tile x, y and z for zoom. 10418 */ 10419 getTileBoundingVolume(tileID, wrap, elevation, options) { 10420 let minElevation = Math.min(0, elevation); 10421 let maxElevation = Math.max(0, elevation); 10422 if (options?.terrain) { 10423 const overscaledTileID = new OverscaledTileID(tileID.z, wrap, tileID.z, tileID.x, tileID.y); 10424 const minMax = options.terrain.getMinMaxElevation(overscaledTileID); 10425 minElevation = minMax.minElevation ?? minElevation; 10426 maxElevation = minMax.maxElevation ?? maxElevation; 10427 } 10428 const numTiles = 1 << tileID.z; 10429 return new Aabb([ 10430 wrap + tileID.x / numTiles, 10431 tileID.y / numTiles, 10432 minElevation 10433 ], [ 10434 wrap + (tileID.x + 1) / numTiles, 10435 (tileID.y + 1) / numTiles, 10436 maxElevation 10437 ]); 10438 } 10439 allowVariableZoom(transform, options) { 10440 const zfov = transform.fov * (Math.abs(Math.cos(transform.rollInRadians)) * transform.height + Math.abs(Math.sin(transform.rollInRadians)) * transform.width) / transform.height; 10441 const maxConstantZoomPitch = clamp(78.5 - zfov / 2, 0, 60); 10442 return !!options.terrain || transform.pitch > maxConstantZoomPitch; 10443 } 10444 allowWorldCopies() { 10445 return true; 10446 } 10447 prepareNextFrame() {} 10448}; 10449//#endregion 10450//#region src/data/pos3d_attributes.ts 10451const pos3dAttributes = createLayout([{ 10452 name: "a_pos3d", 10453 type: "Int16", 10454 components: 3 10455}]); 10456//#endregion 10457//#region src/tile/terrain_tile_manager.ts 10458/** 10459* @internal 10460* This class is a helper for the Terrain-class, it: 10461* 10462* - loads raster-dem tiles 10463* - manages all renderToTexture tiles. 10464* - caches previous rendered tiles. 10465* - finds all necessary renderToTexture tiles for a OverscaledTileID area 10466* - finds the corresponding raster-dem tile for OverscaledTileID 10467*/ 10468var TerrainTileManager = class extends Evented { 10469 constructor(tileManager) { 10470 super(); 10471 this._lastTilesetChange = now(); 10472 this.tileManager = tileManager; 10473 this._tiles = {}; 10474 this._renderableTilesKeys = []; 10475 this._sourceTileCache = {}; 10476 this.minzoom = 0; 10477 this.maxzoom = 22; 10478 this.deltaZoom = 1; 10479 this.tileSize = tileManager._source.tileSize * 2 ** this.deltaZoom; 10480 tileManager.usedForTerrain = true; 10481 tileManager.tileSize = this.tileSize; 10482 } 10483 destruct() { 10484 this.tileManager.usedForTerrain = false; 10485 this.tileManager.tileSize = null; 10486 this.releaseAllRTT(); 10487 } 10488 getSource() { 10489 return this.tileManager._source; 10490 } 10491 /** 10492 * Load Terrain Tiles, create internal render-to-texture tiles, free GPU memory. 10493 * @param transform - the operation to do 10494 * @param terrain - the terrain 10495 * @returns true when the set of renderable tiles changed 10496 */ 10497 update(transform, terrain) { 10498 this.tileManager.update(transform, terrain); 10499 this._renderableTilesKeys = []; 10500 const keys = {}; 10501 let changed = false; 10502 for (const tileID of coveringTiles(transform, { 10503 tileSize: this.tileSize, 10504 minzoom: this.minzoom, 10505 maxzoom: this.maxzoom, 10506 reparseOverscaled: false, 10507 terrain 10508 })) { 10509 keys[tileID.key] = true; 10510 this._renderableTilesKeys.push(tileID.key); 10511 if (!this._tiles[tileID.key]) { 10512 tileID.terrainRttPosMatrix32f = /* @__PURE__ */ new Float32Array(16); 10513 ortho(tileID.terrainRttPosMatrix32f, 0, EXTENT, EXTENT, 0, 0, 1); 10514 this._tiles[tileID.key] = new Tile(tileID, this.tileSize); 10515 this._lastTilesetChange = now(); 10516 changed = true; 10517 } 10518 } 10519 for (const key in this._tiles) if (!keys[key]) { 10520 this._tiles[key].releaseRTT(this.tileManager.map.painter); 10521 delete this._tiles[key]; 10522 changed = true; 10523 } 10524 return changed; 10525 } 10526 /** 10527 * Release the RTT objects for `tileID` (and its ancestors/descendants), 10528 */ 10529 releaseRTT(tileID) { 10530 for (const key in this._tiles) { 10531 const tile = this._tiles[key]; 10532 if (tile.tileID.equals(tileID) || tile.tileID.isChildOf(tileID) || tileID.isChildOf(tile.tileID)) tile.releaseRTT(this.tileManager.map.painter); 10533 } 10534 } 10535 /** 10536 * Release the A RTT objects for all tiles. 10537 */ 10538 releaseAllRTT() { 10539 for (const key in this._tiles) this._tiles[key].releaseRTT(this.tileManager.map.painter); 10540 } 10541 /** 10542 * get a list of tiles, which are loaded and should be rendered in the current scene 10543 * @returns the renderable tiles 10544 */ 10545 getRenderableTiles() { 10546 return this._renderableTilesKeys.map((key) => this.getTileByID(key)); 10547 } 10548 /** 10549 * get terrain tile by the TileID key 10550 * @param id - the tile id 10551 * @returns the tile 10552 */ 10553 getTileByID(id) { 10554 return this._tiles[id]; 10555 } 10556 /** 10557 * Searches for the corresponding current renderable terrain-tiles 10558 * @param tileID - the tile to look for 10559 * @returns the tiles that were found 10560 */ 10561 getTerrainCoords(tileID, terrainTileRanges) { 10562 if (terrainTileRanges) return this._getTerrainCoordsForTileRanges(tileID, terrainTileRanges); 10563 else return this._getTerrainCoordsForRegularTile(tileID); 10564 } 10565 /** 10566 * Searches for the corresponding current renderable terrain-tiles. 10567 * Includes terrain tiles that are either: 10568 * - the same as the tileID 10569 * - a parent of the tileID 10570 * - a child of the tileID 10571 * @param tileID - the tile to look for 10572 * @returns the tiles that were found 10573 */ 10574 _getTerrainCoordsForRegularTile(tileID) { 10575 const coords = {}; 10576 for (const key of this._renderableTilesKeys) { 10577 const terrainTileID = this._tiles[key].tileID; 10578 const coord = tileID.clone(); 10579 const mat = createMat4f64(); 10580 if (terrainTileID.canonical.equals(tileID.canonical)) ortho(mat, 0, EXTENT, EXTENT, 0, 0, 1); 10581 else if (terrainTileID.canonical.isChildOf(tileID.canonical)) { 10582 const dz = terrainTileID.canonical.z - tileID.canonical.z; 10583 const dx = terrainTileID.canonical.x - (terrainTileID.canonical.x >> dz << dz); 10584 const dy = terrainTileID.canonical.y - (terrainTileID.canonical.y >> dz << dz); 10585 const size = EXTENT >> dz; 10586 ortho(mat, 0, size, size, 0, 0, 1); 10587 translate(mat, mat, [ 10588 -dx * size, 10589 -dy * size, 10590 0 10591 ]); 10592 } else if (tileID.canonical.isChildOf(terrainTileID.canonical)) { 10593 const dz = tileID.canonical.z - terrainTileID.canonical.z; 10594 const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz); 10595 const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz); 10596 const size = EXTENT >> dz; 10597 ortho(mat, 0, EXTENT, EXTENT, 0, 0, 1); 10598 translate(mat, mat, [ 10599 dx * size, 10600 dy * size, 10601 0 10602 ]); 10603 scale(mat, mat, [ 10604 1 / 2 ** dz, 10605 1 / 2 ** dz, 10606 0 10607 ]); 10608 } else continue; 10609 coord.terrainRttPosMatrix32f = new Float32Array(mat); 10610 coords[key] = coord; 10611 } 10612 return coords; 10613 } 10614 /** 10615 * Searches for the corresponding current renderable terrain-tiles. 10616 * Includes terrain tiles that are within terrain tile ranges. 10617 * @param tileID - the tile to look for 10618 * @returns the tiles that were found 10619 */ 10620 _getTerrainCoordsForTileRanges(tileID, terrainTileRanges) { 10621 const coords = {}; 10622 for (const key of this._renderableTilesKeys) { 10623 const terrainTileID = this._tiles[key].tileID; 10624 if (!this._isWithinTileRanges(terrainTileID, terrainTileRanges)) continue; 10625 const coord = tileID.clone(); 10626 const mat = createMat4f64(); 10627 if (terrainTileID.canonical.z === tileID.canonical.z) { 10628 const dx = tileID.canonical.x - terrainTileID.canonical.x + tileID.wrap * (1 << tileID.canonical.z); 10629 const dy = tileID.canonical.y - terrainTileID.canonical.y; 10630 ortho(mat, 0, EXTENT, EXTENT, 0, 0, 1); 10631 translate(mat, mat, [ 10632 dx * EXTENT, 10633 dy * EXTENT, 10634 0 10635 ]); 10636 } else if (terrainTileID.canonical.z > tileID.canonical.z) { 10637 const dz = terrainTileID.canonical.z - tileID.canonical.z; 10638 const dx = terrainTileID.canonical.x - (terrainTileID.canonical.x >> dz << dz) + tileID.wrap * (1 << terrainTileID.canonical.z); 10639 const dy = terrainTileID.canonical.y - (terrainTileID.canonical.y >> dz << dz); 10640 const dx2 = tileID.canonical.x - (terrainTileID.canonical.x >> dz); 10641 const dy2 = tileID.canonical.y - (terrainTileID.canonical.y >> dz); 10642 const size = EXTENT >> dz; 10643 ortho(mat, 0, size, size, 0, 0, 1); 10644 translate(mat, mat, [ 10645 -dx * size + dx2 * EXTENT, 10646 -dy * size + dy2 * EXTENT, 10647 0 10648 ]); 10649 } else { 10650 const dz = tileID.canonical.z - terrainTileID.canonical.z; 10651 const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz) + tileID.wrap * (1 << tileID.canonical.z); 10652 const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz); 10653 const dx2 = (tileID.canonical.x >> dz) - terrainTileID.canonical.x; 10654 const dy2 = (tileID.canonical.y >> dz) - terrainTileID.canonical.y; 10655 const size = EXTENT << dz; 10656 ortho(mat, 0, size, size, 0, 0, 1); 10657 translate(mat, mat, [ 10658 dx * EXTENT + dx2 * size, 10659 dy * EXTENT + dy2 * size, 10660 0 10661 ]); 10662 } 10663 coord.terrainRttPosMatrix32f = new Float32Array(mat); 10664 coords[key] = coord; 10665 } 10666 return coords; 10667 } 10668 /** 10669 * find the covering raster-dem tile 10670 * @param tileID - the tile to look for 10671 * @param searchForDEM - Optional parameter to search for (parent) source tiles with loaded dem. 10672 * @returns the tile 10673 */ 10674 getSourceTile(tileID, searchForDEM) { 10675 const source = this.tileManager._source; 10676 let z = tileID.overscaledZ - this.deltaZoom; 10677 if (z > source.maxzoom) z = source.maxzoom; 10678 if (z < source.minzoom) return void 0; 10679 this._sourceTileCache[tileID.key] ||= tileID.scaledTo(z).key; 10680 let tile = this.findTileInCaches(this._sourceTileCache[tileID.key]); 10681 if (!tile?.dem && searchForDEM) while (z >= source.minzoom && !tile?.dem) tile = this.findTileInCaches(tileID.scaledTo(z--).key); 10682 return tile; 10683 } 10684 findTileInCaches(key) { 10685 let tile = this.tileManager.getTileByID(key); 10686 if (tile) return tile; 10687 tile = this.tileManager._outOfViewCache.getByKey(key); 10688 return tile; 10689 } 10690 /** 10691 * gets whether any tiles were loaded after a specific time. This is used to update the depth framebuffer. 10692 * @param time - the time 10693 * @returns true if any tiles came into view at or after the specified time 10694 */ 10695 anyTilesAfterTime(time = now()) { 10696 return this._lastTilesetChange >= time; 10697 } 10698 /** 10699 * Checks whether a tile is within the canonical tile ranges. 10700 * @param tileID - Tile to check 10701 * @param canonicalTileRanges - Canonical tile ranges 10702 * @returns 10703 */ 10704 _isWithinTileRanges(tileID, canonicalTileRanges) { 10705 const range = canonicalTileRanges[tileID.canonical.z]; 10706 return !!range && (tileID.wrap > range.minWrap || tileID.wrap < range.maxWrap || tileID.canonical.x >= range.minTileXWrapped && tileID.canonical.x <= range.maxTileXWrapped && tileID.canonical.y >= range.minTileY && tileID.canonical.y <= range.maxTileY); 10707 } 10708}; 10709//#endregion 10710//#region src/render/terrain.ts 10711const MAX_BISECTIONS = 40; 10712const HIT_EPSILON_M = 1e-6; 10713/** Keeps the elevation bracket non-degenerate when the terrain is entirely flat, such as unloaded DEMs. */ 10714const BRACKET_PADDING_M = 10; 10715/** Tile coordinates run from 0 up to but not including `EXTENT`; a point on the far edge is clamped back into the tile. */ 10716const MAX_TILE_COORD = EXTENT * (1 - 1e-12); 10717/** 10718* Offset, in DEM pixels, from a tile coordinate scaled by `dim` to the pixel index `DEMData.sampleBilinear` expects. 10719* DEM pixel `i` describes the cell centred at tile coordinate `(i + 0.5) / dim`, the same placement hillshade and 10720* color-relief use, so a sample between two cell centres interpolates the pixels on either side of it. 10721*/ 10722const DEM_CELL_CENTER_OFFSET = -.5; 10723/** 10724* @internal 10725* This is the main class which handles most of the 3D Terrain logic. It has the following topics: 10726* 10727* 1. loads raster-dem tiles via the internal tileManager this.tileManager 10728* 2. creates a depth-framebuffer, which is used to calculate the visibility of coordinates 10729* 3. stores all render-to-texture tiles in the this.tileManager._tiles 10730* 4. calculates the elevation for a specific tile-coordinate 10731* 5. creates a terrain-mesh 10732* 10733* A note about the GPU resource-usage: 10734* 10735* Framebuffers: 10736* 10737* - one for the depth framebuffer with the size of the map-div. 10738* - one for rendering a tile to texture with the size of tileSize (= 512x512). 10739* 10740* Textures: 10741* 10742* - one texture for an empty raster-dem tile with size 1x1 10743* - one texture for an empty depth-buffer, when terrain is disabled with size 1x1 10744* - one texture for an each loaded raster-dem with size of the source.tileSize 10745* - one texture for the depth-framebuffer with the size of the map-div. 10746* - finally for each render-to-texture tile (= this._tiles) a set of textures 10747* for each render stack (The stack-concept is documented in painter.ts). 10748* 10749* Normally there exists 1-3 Textures per tile, depending on the stylesheet. 10750* Each Textures has the size 2*tileSize (= 1024x1024). Also there exists a 10751* cache of the last 150 newest rendered tiles. 10752* 10753*/ 10754var Terrain = class { 10755 constructor(painter, tileManager, options, terrainSkirtLength = "auto") { 10756 this._meshCache = {}; 10757 this.painter = painter; 10758 this.tileManager = new TerrainTileManager(tileManager); 10759 this.options = options; 10760 this.exaggeration = typeof options.exaggeration === "number" ? options.exaggeration : 1; 10761 this._terrainSkirtLength = terrainSkirtLength; 10762 this.qualityFactor = 2; 10763 this.meshSize = 128; 10764 this._demMatrixCache = /* @__PURE__ */ new Map(); 10765 this._elevationSamplerCache = /* @__PURE__ */ new Map(); 10766 } 10767 destroy() { 10768 if (this._fbo) { 10769 this._fbo.destroy(); 10770 this._fbo = null; 10771 } 10772 if (this._fboDepthTexture) { 10773 this._fboDepthTexture.destroy(); 10774 this._fboDepthTexture = null; 10775 } 10776 if (this._emptyDemTexture) { 10777 this._emptyDemTexture.destroy(); 10778 this._emptyDemTexture = null; 10779 } 10780 if (this._emptyDepthTexture) { 10781 this._emptyDepthTexture.destroy(); 10782 this._emptyDepthTexture = null; 10783 } 10784 for (const key in this._meshCache) this._meshCache[key].destroy(); 10785 this._meshCache = {}; 10786 this.tileManager.destruct(); 10787 } 10788 /** 10789 * Get the elevation-value from original dem-data for a given tile-coordinate. 10790 * Coordinates that fall outside `[0, extent)` are normalized to the 10791 * appropriate neighbor tile before lookup. 10792 * @param tileID - the tile to get the elevation for 10793 * @param x - x coordinate relative to the tile, may be outside `[0, extent)` 10794 * @param y - y coordinate relative to the tile, may be outside `[0, extent)` 10795 * @param extent - optional, default 8192 10796 * @returns the elevation 10797 */ 10798 getDEMElevation(tileID, x, y, extent = EXTENT) { 10799 const normalized = tileID.normalizeCoordinates(x, y, extent); 10800 if (!normalized) return 0; 10801 const sampler = this.getElevationSampler(normalized.tileID); 10802 return sampler ? sampler(normalized.x, normalized.y, extent) : 0; 10803 } 10804 /** 10805 * Get the elevation for given {@link LngLat} in respect of exaggeration. 10806 * @param lnglat - the location 10807 * @param zoom - the zoom, use {@link getElevationForLngLat} if you don't want a specific zoom level, but more accurate results. 10808 * @returns the elevation 10809 */ 10810 getElevationForLngLatZoom(lnglat, zoom) { 10811 if (!isInBoundsForZoomLngLat(zoom, lnglat.wrap())) return 0; 10812 const { tileID, mercatorX, mercatorY } = this._getOverscaledTileIDFromLngLatZoom(lnglat, zoom); 10813 return this.getElevation(tileID, mercatorX % EXTENT, mercatorY % EXTENT, EXTENT); 10814 } 10815 /** 10816 * Get the elevation for given {@link LngLat} in respect of exaggeration. 10817 * Where the location is covered by a rendered tile with loaded DEM data this samples the 10818 * rendered surface, so the result agrees with what is drawn; elsewhere it traverses up the 10819 * zoom levels to find the first tile with data to return. 10820 * @param lnglat - the location 10821 * @returns the elevation 10822 */ 10823 getElevationForLngLat(lnglat, transform) { 10824 const index = this.getCoverageIndex(); 10825 if (index) { 10826 const mercator = MercatorCoordinate.fromLngLat(lnglat); 10827 const sample = sampleAt(index, this.exaggeration, mercator.x, mercator.y); 10828 if (sample.demLoaded) return sample.elevation; 10829 } 10830 const terrainCoveringTiles = coveringTiles(transform, { 10831 maxzoom: this.tileManager.maxzoom, 10832 minzoom: this.tileManager.minzoom, 10833 tileSize: 512, 10834 terrain: this 10835 }); 10836 let zoom = 0; 10837 for (const tile of terrainCoveringTiles) if (tile.canonical.z > zoom) zoom = Math.min(tile.canonical.z, this.tileManager.maxzoom); 10838 return this.getElevationForLngLatZoom(lnglat, zoom); 10839 } 10840 /** 10841 * Get the elevation for given coordinate in respect of exaggeration. 10842 * @param tileID - the tile id 10843 * @param x - x coordinate relative to the tile, may be outside `[0, extent)` 10844 * @param y - y coordinate relative to the tile, may be outside `[0, extent)` 10845 * @param extent - optional, default 8192 10846 * @returns the elevation 10847 */ 10848 getElevation(tileID, x, y, extent = EXTENT) { 10849 return this.getDEMElevation(tileID, x, y, extent) * this.exaggeration; 10850 } 10851 /** 10852 * Clear CPU elevation samplers that may retain a previously selected DEM tile. 10853 * @internal 10854 */ 10855 resetElevationCache() { 10856 this._elevationSamplerCache.clear(); 10857 this._coverageIndex = void 0; 10858 } 10859 /** 10860 * Index of the tiles the terrain currently renders, for sampling the terrain surface on the CPU. 10861 * Built on first use and kept until {@link resetElevationCache}. 10862 * @returns the index, or null when no terrain tile is renderable 10863 */ 10864 getCoverageIndex() { 10865 if (this._coverageIndex === void 0) this._coverageIndex = this._buildCoverageIndex(); 10866 return this._coverageIndex; 10867 } 10868 _buildCoverageIndex() { 10869 const zooms = []; 10870 const samplerPerTile = /* @__PURE__ */ new Map(); 10871 let minElevation = 0; 10872 let maxElevation = 0; 10873 for (const tile of this.tileManager.getRenderableTiles()) { 10874 if (!tile) continue; 10875 const { canonical, wrap } = tile.tileID; 10876 if (!zooms.includes(canonical.z)) zooms.push(canonical.z); 10877 const sampler = this.getElevationSampler(tile.tileID); 10878 samplerPerTile.set(`${wrap}/${canonical.z}/${canonical.x}/${canonical.y}`, sampler); 10879 const { minElevation: tileMin, maxElevation: tileMax } = this.getMinMaxElevation(tile.tileID); 10880 minElevation = Math.min(minElevation, tileMin ?? 0); 10881 maxElevation = Math.max(maxElevation, tileMax ?? 0); 10882 } 10883 if (samplerPerTile.size === 0) return null; 10884 zooms.sort((a, b) => b - a); 10885 return { 10886 zooms, 10887 samplerPerTile, 10888 minElevation: minElevation - BRACKET_PADDING_M, 10889 maxElevation: maxElevation + BRACKET_PADDING_M 10890 }; 10891 } 10892 /** 10893 * Get a function that samples the raw DEM elevation of a tile, without exaggeration. 10894 * The sampler places DEM pixels at cell centres, matching `get_elevation` in the vertex shader prelude. 10895 * @param tileID - the tile id 10896 * @returns the sampler, or null when the tile's DEM data is not loaded 10897 */ 10898 getElevationSampler(tileID) { 10899 const key = tileID.key; 10900 const cachedSampler = this._elevationSamplerCache.get(key); 10901 if (cachedSampler) return cachedSampler; 10902 const sourceTile = this.tileManager.getSourceTile(tileID, true); 10903 const dem = sourceTile?.dem; 10904 if (!sourceTile || !dem) return null; 10905 const matrix = this._getDEMTileMatrix(tileID, sourceTile); 10906 const demPixelScaleX = matrix[0] * dem.dim; 10907 const demPixelScaleY = matrix[5] * dem.dim; 10908 const demPixelOffsetX = matrix[12] * dem.dim + DEM_CELL_CENTER_OFFSET; 10909 const demPixelOffsetY = matrix[13] * dem.dim + DEM_CELL_CENTER_OFFSET; 10910 const sampler = (x, y, extent) => { 10911 const extentScale = extent === 8192 ? 1 : EXTENT / extent; 10912 return dem.sampleBilinear(x * extentScale * demPixelScaleX + demPixelOffsetX, y * extentScale * demPixelScaleY + demPixelOffsetY); 10913 }; 10914 this._elevationSamplerCache.set(key, sampler); 10915 return sampler; 10916 } 10917 /** 10918 * Get the matrix that maps a tile's coordinates into the DEM tile it is rendered with. 10919 * The transform is derived from the loaded DEM tile's own zoom level, not from the source's 10920 * declared maxzoom: getSourceTile falls back to a loaded parent tile while the deepest DEM 10921 * tile is still loading, and the scale and offset must match the tile that is actually used. 10922 * @param tileID - the tile id 10923 * @param sourceTile - the DEM tile that is used for this tile, either its own tile or a loaded parent 10924 * @returns the matrix that maps the tile's coordinates onto the DEM tile 10925 */ 10926 _getDEMTileMatrix(tileID, sourceTile) { 10927 const matrixKey = `${sourceTile.tileID.key}/${tileID.key}`; 10928 const cachedMatrix = this._demMatrixCache.get(matrixKey); 10929 if (cachedMatrix) return cachedMatrix; 10930 const dz = tileID.canonical.z - sourceTile.tileID.canonical.z; 10931 const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz); 10932 const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz); 10933 const demMatrix = fromScaling(/* @__PURE__ */ new Float64Array(16), [ 10934 1 / (EXTENT << dz), 10935 1 / (EXTENT << dz), 10936 0 10937 ]); 10938 translate(demMatrix, demMatrix, [ 10939 dx * EXTENT, 10940 dy * EXTENT, 10941 0 10942 ]); 10943 this._demMatrixCache.set(matrixKey, demMatrix); 10944 return demMatrix; 10945 } 10946 /** 10947 * returns a Terrain Object for a tile. Unless the tile corresponds to data (e.g. tile is loading), return a flat dem object 10948 * @param tileID - the tile to get the terrain for 10949 * @returns the terrain data to use in the program 10950 */ 10951 getTerrainData(tileID) { 10952 if (!this._emptyDemTexture) { 10953 const context = this.painter.context; 10954 const image = new RGBAImage({ 10955 width: 1, 10956 height: 1 10957 }, /* @__PURE__ */ new Uint8Array(4)); 10958 this._emptyDepthTexture = new Texture(context, image, context.gl.RGBA, { premultiply: false }); 10959 this._emptyDemUnpack = [ 10960 0, 10961 0, 10962 0, 10963 0 10964 ]; 10965 this._emptyDemTexture = new Texture(context, new RGBAImage({ 10966 width: 1, 10967 height: 1 10968 }), context.gl.RGBA, { premultiply: false }); 10969 this._emptyDemTexture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE); 10970 this._emptyDemMatrix = identity([]); 10971 } 10972 const sourceTile = this.tileManager.getSourceTile(tileID, true); 10973 if (sourceTile?.dem && (!sourceTile.demTexture || sourceTile.needsTerrainPrepare)) { 10974 const context = this.painter.context; 10975 sourceTile.demTexture ||= this.painter.getTileTexture(sourceTile.dem.stride); 10976 if (sourceTile.demTexture) sourceTile.demTexture.update(sourceTile.dem.getPixels(), { premultiply: false }); 10977 else sourceTile.demTexture = new Texture(context, sourceTile.dem.getPixels(), context.gl.RGBA, { premultiply: false }); 10978 sourceTile.demTexture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE); 10979 sourceTile.needsTerrainPrepare = false; 10980 } 10981 const terrainMatrix = sourceTile ? this._getDEMTileMatrix(tileID, sourceTile) : this._emptyDemMatrix; 10982 return { 10983 "u_depth": 2, 10984 "u_terrain": 3, 10985 "u_terrain_dim": sourceTile?.dem?.dim || 1, 10986 "u_terrain_matrix": terrainMatrix, 10987 "u_terrain_unpack": sourceTile?.dem?.getUnpackVector() || this._emptyDemUnpack, 10988 "u_terrain_exaggeration": this.exaggeration, 10989 texture: (sourceTile?.demTexture || this._emptyDemTexture).texture, 10990 depthTexture: (this._fboDepthTexture || this._emptyDepthTexture).texture, 10991 tile: sourceTile 10992 }; 10993 } 10994 /** 10995 * get a framebuffer as big as the map-div, which will be used to render depth into a texture 10996 * @returns the frame buffer 10997 */ 10998 getFramebuffer() { 10999 const painter = this.painter; 11000 const width = painter.width / devicePixelRatio; 11001 const height = painter.height / devicePixelRatio; 11002 if (this._fbo && (this._fbo.width !== width || this._fbo.height !== height)) { 11003 this._fbo.destroy(); 11004 this._fboDepthTexture.destroy(); 11005 delete this._fbo; 11006 delete this._fboDepthTexture; 11007 } 11008 if (!this._fboDepthTexture) { 11009 this._fboDepthTexture = new Texture(painter.context, { 11010 width, 11011 height, 11012 data: null 11013 }, painter.context.gl.RGBA, { premultiply: false }); 11014 this._fboDepthTexture.bind(painter.context.gl.NEAREST, painter.context.gl.CLAMP_TO_EDGE); 11015 } 11016 if (!this._fbo) { 11017 this._fbo = painter.context.createFramebuffer(width, height, true, false); 11018 this._fbo.depthAttachment.set(painter.context.createRenderbuffer(painter.context.gl.DEPTH_COMPONENT16, width, height)); 11019 } 11020 this._fbo.colorAttachment.set(this._fboDepthTexture.texture); 11021 return this._fbo; 11022 } 11023 /** 11024 * create a regular mesh which will be used by all terrain-tiles 11025 * @returns the created regular mesh 11026 */ 11027 getTerrainMesh(tileId) { 11028 const globeEnabled = this.painter.style.projection?.transitionState > 0; 11029 const northPole = globeEnabled && tileId.canonical.y === 0; 11030 const southPole = globeEnabled && tileId.canonical.y === (1 << tileId.canonical.z) - 1; 11031 const key = `m_${northPole ? "n" : ""}_${southPole ? "s" : ""}`; 11032 if (this._meshCache[key]) return this._meshCache[key]; 11033 const context = this.painter.context; 11034 const vertexArray = new Pos3dArray(); 11035 const indexArray = new TriangleIndexArray(); 11036 const meshSize = this.meshSize; 11037 const delta = EXTENT / meshSize; 11038 const meshSize2 = meshSize * meshSize; 11039 for (let y = 0; y <= meshSize; y++) for (let x = 0; x <= meshSize; x++) vertexArray.emplaceBack(x * delta, y * delta, 0); 11040 for (let y = 0; y < meshSize2; y += meshSize + 1) for (let x = 0; x < meshSize; x++) { 11041 indexArray.emplaceBack(x + y, meshSize + x + y + 1, meshSize + x + y + 2); 11042 indexArray.emplaceBack(x + y, meshSize + x + y + 2, x + y + 1); 11043 } 11044 if (this._terrainSkirtLength !== "none") this._buildSkirts(vertexArray, indexArray, meshSize, delta, northPole, southPole); 11045 const mesh = new Mesh(context.createVertexBuffer(vertexArray, pos3dAttributes.members), context.createIndexBuffer(indexArray), SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length)); 11046 this._meshCache[key] = mesh; 11047 return mesh; 11048 } 11049 /** 11050 * Calculates the height of the tile skirts for the "auto" strategy. 11051 * @see {@link MapOptions.terrainSkirtLength} 11052 * @param zoom - current zoomlevel 11053 * @returns the elevation delta in meters 11054 */ 11055 getSkirtLength(zoom) { 11056 return 2 * Math.PI * earthRadius / Math.pow(2, Math.max(zoom, 0)) / 5; 11057 } 11058 getMinTileElevationForLngLatZoom(lnglat, zoom) { 11059 if (!isInBoundsForZoomLngLat(zoom, lnglat.wrap())) return 0; 11060 const { tileID } = this._getOverscaledTileIDFromLngLatZoom(lnglat, zoom); 11061 return this.getMinMaxElevation(tileID).minElevation ?? 0; 11062 } 11063 /** 11064 * Get the minimum and maximum elevation contained in a tile. This includes any 11065 * exaggeration included in the terrain. 11066 * 11067 * @param tileID - ID of the tile to be used as a source for the min/max elevation 11068 * @returns the minimum and maximum elevation found in the tile, including the terrain's 11069 * exaggeration 11070 */ 11071 getMinMaxElevation(tileID) { 11072 const tile = this.tileManager.getSourceTile(tileID, true); 11073 const minMax = { 11074 minElevation: null, 11075 maxElevation: null 11076 }; 11077 if (tile?.dem) { 11078 minMax.minElevation = tile.dem.min * this.exaggeration; 11079 minMax.maxElevation = tile.dem.max * this.exaggeration; 11080 } 11081 return minMax; 11082 } 11083 _getOverscaledTileIDFromLngLatZoom(lnglat, zoom) { 11084 const mercatorCoordinate = MercatorCoordinate.fromLngLat(lnglat.wrap()); 11085 const worldSize = (1 << zoom) * EXTENT; 11086 const mercatorX = mercatorCoordinate.x * worldSize; 11087 const mercatorY = mercatorCoordinate.y * worldSize; 11088 const tileX = Math.floor(mercatorX / EXTENT), tileY = Math.floor(mercatorY / EXTENT); 11089 return { 11090 tileID: new OverscaledTileID(zoom, 0, zoom, tileX, tileY), 11091 mercatorX, 11092 mercatorY 11093 }; 11094 } 11095 /** Add an extra frame around the mesh to avoid hairline gaps (stitching) on tile boundaries with different zoomlevels. 11096 * @see {@link MapOptions.terrainSkirtLength} 11097 */ 11098 _buildSkirts(vertexArray, indexArray, meshSize, delta, northPole, southPole) { 11099 const offsetTop = vertexArray.length; 11100 const offsetTopEdge = 0; 11101 const offsetBottom = offsetTop + (meshSize + 1); 11102 const offsetBottomEdge = (meshSize + 1) * meshSize; 11103 const northY = northPole ? NORTH_POLE_Y : 0; 11104 const northZ = northPole ? 0 : 1; 11105 const southY = southPole ? SOUTH_POLE_Y : EXTENT; 11106 const southZ = southPole ? 0 : 1; 11107 for (let x = 0; x <= meshSize; x++) vertexArray.emplaceBack(x * delta, northY, northZ); 11108 for (let x = 0; x <= meshSize; x++) vertexArray.emplaceBack(x * delta, southY, southZ); 11109 for (let x = 0; x < meshSize; x++) { 11110 indexArray.emplaceBack(offsetBottomEdge + x, offsetBottom + x, offsetBottom + x + 1); 11111 indexArray.emplaceBack(offsetBottomEdge + x, offsetBottom + x + 1, offsetBottomEdge + x + 1); 11112 indexArray.emplaceBack(offsetTopEdge + x, offsetTop + x + 1, offsetTop + x); 11113 indexArray.emplaceBack(offsetTopEdge + x, offsetTopEdge + x + 1, offsetTop + x + 1); 11114 } 11115 const offsetLeft = vertexArray.length; 11116 const offsetRight = offsetLeft + (meshSize + 1) * 2; 11117 for (const x of [0, 1]) for (let y = 0; y <= meshSize; y++) for (const z of [0, 1]) vertexArray.emplaceBack(x * EXTENT, y * delta, z); 11118 for (let y = 0; y < meshSize * 2; y += 2) { 11119 indexArray.emplaceBack(offsetLeft + y, offsetLeft + y + 1, offsetLeft + y + 3); 11120 indexArray.emplaceBack(offsetLeft + y, offsetLeft + y + 3, offsetLeft + y + 2); 11121 indexArray.emplaceBack(offsetRight + y, offsetRight + y + 3, offsetRight + y + 1); 11122 indexArray.emplaceBack(offsetRight + y, offsetRight + y + 2, offsetRight + y + 3); 11123 } 11124 } 11125}; 11126const NOT_COVERED = { 11127 covered: false, 11128 demLoaded: false, 11129 elevation: 0 11130}; 11131/** 11132* Elevation of the rendered terrain surface at a mercator position, and whether it is covered at all. 11133* A covered tile whose DEM has not loaded yet is flat at zero, which is what the terrain mesh renders. 11134*/ 11135function sampleAt(index, exaggeration, mercatorX, mercatorY) { 11136 if (mercatorY < 0 || mercatorY >= 1) return NOT_COVERED; 11137 const wrap = Math.floor(mercatorX); 11138 const wrappedX = mercatorX - wrap; 11139 for (const z of index.zooms) { 11140 const scale = 1 << z; 11141 const scaledX = wrappedX * scale; 11142 const scaledY = mercatorY * scale; 11143 const tileX = Math.floor(scaledX); 11144 const tileY = Math.floor(scaledY); 11145 const key = `${wrap}/${z}/${tileX}/${tileY}`; 11146 if (!index.samplerPerTile.has(key)) continue; 11147 const sampler = index.samplerPerTile.get(key); 11148 if (!sampler) return { 11149 covered: true, 11150 demLoaded: false, 11151 elevation: 0 11152 }; 11153 return { 11154 covered: true, 11155 demLoaded: true, 11156 elevation: sampler(Math.min((scaledX - tileX) * EXTENT, MAX_TILE_COORD), Math.min((scaledY - tileY) * EXTENT, MAX_TILE_COORD), EXTENT) * exaggeration 11157 }; 11158 } 11159 return NOT_COVERED; 11160} 11161/** 11162* Whether a height in meters is at or below the sampled terrain surface. 11163* The epsilon absorbs rounding when a bracket endpoint lands exactly on the surface. 11164*/ 11165function isBelowTerrainSample(sample, height) { 11166 return sample.covered && height <= sample.elevation + HIT_EPSILON_M; 11167} 11168/** 11169* Narrows the bracket `[lo, hi]` around the surface crossing until it is shorter than `tolerance` in ray parameter units. 11170*/ 11171function bisect(ray, isBelowTerrain, lo, hi, tolerance) { 11172 for (let j = 0; j < MAX_BISECTIONS && hi - lo > tolerance; j++) { 11173 const mid = (lo + hi) / 2; 11174 if (isBelowTerrain(ray, mid)) hi = mid; 11175 else lo = mid; 11176 } 11177 return { 11178 lo, 11179 hi 11180 }; 11181} 11182//#endregion 11183//#region src/geo/projection/mercator_transform.ts 11184const TARGET_WORLD_STEP_PX = 4; 11185const MAX_SAMPLES = 512; 11186const MERCATOR_BISECT_EPSILON_WORLD_PX = .001; 11187var MercatorTransform = class MercatorTransform { 11188 get pixelsToClipSpaceMatrix() { 11189 return this._helper.pixelsToClipSpaceMatrix; 11190 } 11191 get clipSpaceToPixelsMatrix() { 11192 return this._helper.clipSpaceToPixelsMatrix; 11193 } 11194 get pixelsToGLUnits() { 11195 return this._helper.pixelsToGLUnits; 11196 } 11197 get centerOffset() { 11198 return this._helper.centerOffset; 11199 } 11200 get size() { 11201 return this._helper.size; 11202 } 11203 get rotationMatrix() { 11204 return this._helper.rotationMatrix; 11205 } 11206 get centerPoint() { 11207 return this._helper.centerPoint; 11208 } 11209 get pixelsPerMeter() { 11210 return this._helper.pixelsPerMeter; 11211 } 11212 setMinZoom(zoom) { 11213 this._helper.setMinZoom(zoom); 11214 } 11215 setMaxZoom(zoom) { 11216 this._helper.setMaxZoom(zoom); 11217 } 11218 setMinPitch(pitch) { 11219 this._helper.setMinPitch(pitch); 11220 } 11221 setMaxPitch(pitch) { 11222 this._helper.setMaxPitch(pitch); 11223 } 11224 setRenderWorldCopies(renderWorldCopies) { 11225 this._helper.setRenderWorldCopies(renderWorldCopies); 11226 } 11227 setBearing(bearing) { 11228 this._helper.setBearing(bearing); 11229 } 11230 setPitch(pitch) { 11231 this._helper.setPitch(pitch); 11232 } 11233 setRoll(roll) { 11234 this._helper.setRoll(roll); 11235 } 11236 setFov(fov) { 11237 this._helper.setFov(fov); 11238 } 11239 setZoom(zoom) { 11240 this._helper.setZoom(zoom); 11241 } 11242 setCenter(center) { 11243 this._helper.setCenter(center); 11244 } 11245 setElevation(elevation) { 11246 this._helper.setElevation(elevation); 11247 } 11248 setMinElevationForCurrentTile(elevation) { 11249 this._helper.setMinElevationForCurrentTile(elevation); 11250 } 11251 setPadding(padding) { 11252 this._helper.setPadding(padding); 11253 } 11254 interpolatePadding(start, target, t) { 11255 this._helper.interpolatePadding(start, target, t); 11256 } 11257 isPaddingEqual(padding) { 11258 return this._helper.isPaddingEqual(padding); 11259 } 11260 resize(width, height, constrain = true) { 11261 this._helper.resize(width, height, constrain); 11262 } 11263 getMaxBounds() { 11264 return this._helper.getMaxBounds(); 11265 } 11266 setMaxBounds(bounds) { 11267 this._helper.setMaxBounds(bounds); 11268 } 11269 setConstrainOverride(constrain) { 11270 this._helper.setConstrainOverride(constrain); 11271 } 11272 overrideNearFarZ(nearZ, farZ) { 11273 this._helper.overrideNearFarZ(nearZ, farZ); 11274 } 11275 clearNearFarZOverride() { 11276 this._helper.clearNearFarZOverride(); 11277 } 11278 getCameraQueryGeometry(queryGeometry) { 11279 return this._helper.getCameraQueryGeometry(this.getCameraPoint(), queryGeometry); 11280 } 11281 get tileSize() { 11282 return this._helper.tileSize; 11283 } 11284 get tileZoom() { 11285 return this._helper.tileZoom; 11286 } 11287 get scale() { 11288 return this._helper.scale; 11289 } 11290 get worldSize() { 11291 return this._helper.worldSize; 11292 } 11293 get width() { 11294 return this._helper.width; 11295 } 11296 get height() { 11297 return this._helper.height; 11298 } 11299 get lngRange() { 11300 return this._helper.lngRange; 11301 } 11302 get latRange() { 11303 return this._helper.latRange; 11304 } 11305 get minZoom() { 11306 return this._helper.minZoom; 11307 } 11308 get maxZoom() { 11309 return this._helper.maxZoom; 11310 } 11311 get zoom() { 11312 return this._helper.zoom; 11313 } 11314 get center() { 11315 return this._helper.center; 11316 } 11317 get minPitch() { 11318 return this._helper.minPitch; 11319 } 11320 get maxPitch() { 11321 return this._helper.maxPitch; 11322 } 11323 get pitch() { 11324 return this._helper.pitch; 11325 } 11326 get pitchInRadians() { 11327 return this._helper.pitchInRadians; 11328 } 11329 get roll() { 11330 return this._helper.roll; 11331 } 11332 get rollInRadians() { 11333 return this._helper.rollInRadians; 11334 } 11335 get bearing() { 11336 return this._helper.bearing; 11337 } 11338 get bearingInRadians() { 11339 return this._helper.bearingInRadians; 11340 } 11341 get fov() { 11342 return this._helper.fov; 11343 } 11344 get fovInRadians() { 11345 return this._helper.fovInRadians; 11346 } 11347 get elevation() { 11348 return this._helper.elevation; 11349 } 11350 get minElevationForCurrentTile() { 11351 return this._helper.minElevationForCurrentTile; 11352 } 11353 get padding() { 11354 return this._helper.padding; 11355 } 11356 get unmodified() { 11357 return this._helper.unmodified; 11358 } 11359 get renderWorldCopies() { 11360 return this._helper.renderWorldCopies; 11361 } 11362 get cameraToCenterDistance() { 11363 return this._helper.cameraToCenterDistance; 11364 } 11365 get constrainOverride() { 11366 return this._helper.constrainOverride; 11367 } 11368 get nearZ() { 11369 return this._helper.nearZ; 11370 } 11371 get farZ() { 11372 return this._helper.farZ; 11373 } 11374 get autoCalculateNearFarZ() { 11375 return this._helper.autoCalculateNearFarZ; 11376 } 11377 setTransitionState(_value) {} 11378 /** 11379 * @param options - Initial state. Ignored when `sharedHelper` is given, which already carries it. 11380 * @param sharedHelper - Camera to use instead of owning one, so that a composing transform such as 11381 * {@link GlobeTransform} keeps a single copy of the state rather than one per child. Its owner then drives 11382 * {@link _calcMatrices}, because a helper has only one `calcMatrices` callback. 11383 */ 11384 constructor(options, sharedHelper) { 11385 this._posMatrixCache = /* @__PURE__ */ new Map(); 11386 this._alignedPosMatrixCache = /* @__PURE__ */ new Map(); 11387 this._fogMatrixCacheF32 = /* @__PURE__ */ new Map(); 11388 this.defaultConstrain = (lngLat, zoom) => { 11389 zoom = clamp(+zoom, this.minZoom, this.maxZoom); 11390 const result = { 11391 center: new LngLat(lngLat.lng, lngLat.lat), 11392 zoom 11393 }; 11394 let lngRange = this._helper._lngRange; 11395 if (!this._helper._renderWorldCopies && lngRange === null) lngRange = [-179.9999999999, 180 - 1e-10]; 11396 const worldSize = this.tileSize * zoomScale(result.zoom); 11397 let minY = 0; 11398 let maxY = worldSize; 11399 let minX = 0; 11400 let maxX = worldSize; 11401 let scaleY = 0; 11402 let scaleX = 0; 11403 const { x: screenWidth, y: screenHeight } = this.size; 11404 if (this._helper._latRange) { 11405 const latRange = this._helper._latRange; 11406 minY = mercatorYfromLat(latRange[1]) * worldSize; 11407 maxY = mercatorYfromLat(latRange[0]) * worldSize; 11408 if (maxY - minY < screenHeight) scaleY = screenHeight / (maxY - minY); 11409 } 11410 if (lngRange) { 11411 minX = wrap(mercatorXfromLng(lngRange[0]) * worldSize, 0, worldSize); 11412 maxX = wrap(mercatorXfromLng(lngRange[1]) * worldSize, 0, worldSize); 11413 if (maxX < minX) maxX += worldSize; 11414 if (maxX - minX < screenWidth) scaleX = screenWidth / (maxX - minX); 11415 } 11416 const { x: originalX, y: originalY } = projectToWorldCoordinates(worldSize, lngLat); 11417 let modifiedX, modifiedY; 11418 const scale = Math.max(scaleX || 0, scaleY || 0); 11419 if (scale) { 11420 const newPoint = new Point(scaleX ? (maxX + minX) / 2 : originalX, scaleY ? (maxY + minY) / 2 : originalY); 11421 result.center = unprojectFromWorldCoordinates(worldSize, newPoint).wrap(); 11422 result.zoom += scaleZoom(scale); 11423 return result; 11424 } 11425 if (this._helper._latRange) { 11426 const h2 = screenHeight / 2; 11427 if (originalY - h2 < minY) modifiedY = minY + h2; 11428 if (originalY + h2 > maxY) modifiedY = maxY - h2; 11429 } 11430 if (lngRange) { 11431 const centerX = (minX + maxX) / 2; 11432 let wrappedX = originalX; 11433 if (this._helper._renderWorldCopies) wrappedX = wrap(originalX, centerX - worldSize / 2, centerX + worldSize / 2); 11434 const w2 = screenWidth / 2; 11435 if (wrappedX - w2 < minX) modifiedX = minX + w2; 11436 if (wrappedX + w2 > maxX) modifiedX = maxX - w2; 11437 } 11438 if (modifiedX !== void 0 || modifiedY !== void 0) { 11439 const newPoint = new Point(modifiedX ?? originalX, modifiedY ?? originalY); 11440 result.center = unprojectFromWorldCoordinates(worldSize, newPoint).wrap(); 11441 } 11442 return result; 11443 }; 11444 this.applyConstrain = (lngLat, zoom) => { 11445 return this._helper.applyConstrain(lngLat, zoom); 11446 }; 11447 this._helper = sharedHelper ?? new TransformHelper({ 11448 calcMatrices: () => this._calcMatrices(), 11449 defaultConstrain: (center, zoom) => { 11450 return this.defaultConstrain(center, zoom); 11451 } 11452 }, options); 11453 this._coveringTilesDetailsProvider = new MercatorCoveringTilesDetailsProvider(); 11454 } 11455 clone() { 11456 const clone = new MercatorTransform(); 11457 clone.apply(this, false); 11458 return clone; 11459 } 11460 apply(that, constrain) { 11461 this._helper.apply(that, constrain); 11462 } 11463 get cameraPosition() { 11464 return this._cameraPosition; 11465 } 11466 get projectionMatrix() { 11467 return this._projectionMatrix; 11468 } 11469 get modelViewProjectionMatrix() { 11470 return this._viewProjMatrix; 11471 } 11472 get inverseProjectionMatrix() { 11473 return this._invProjMatrix; 11474 } 11475 get mercatorMatrix() { 11476 return this._mercatorMatrix; 11477 } 11478 getVisibleUnwrappedCoordinates(tileID) { 11479 const result = [new UnwrappedTileID(0, tileID)]; 11480 if (this._helper._renderWorldCopies) { 11481 const utl = this.screenPointToMercatorCoordinate(new Point(0, 0)); 11482 const utr = this.screenPointToMercatorCoordinate(new Point(this._helper._width, 0)); 11483 const ubl = this.screenPointToMercatorCoordinate(new Point(this._helper._width, this._helper._height)); 11484 const ubr = this.screenPointToMercatorCoordinate(new Point(0, this._helper._height)); 11485 const w0 = Math.floor(Math.min(utl.x, utr.x, ubl.x, ubr.x)); 11486 const w1 = Math.floor(Math.max(utl.x, utr.x, ubl.x, ubr.x)); 11487 const extraWorldCopy = 1; 11488 for (let w = w0 - extraWorldCopy; w <= w1 + extraWorldCopy; w++) { 11489 if (w === 0) continue; 11490 result.push(new UnwrappedTileID(w, tileID)); 11491 } 11492 } 11493 return result; 11494 } 11495 getCameraFrustum() { 11496 return Frustum.fromInvProjectionMatrix(this._invViewProjMatrix, this.worldSize); 11497 } 11498 getClippingPlane() { 11499 return null; 11500 } 11501 getCoveringTilesDetailsProvider() { 11502 return this._coveringTilesDetailsProvider; 11503 } 11504 recalculateZoomAndCenter(terrain) { 11505 const center = this.screenPointToLocation(this.centerPoint, terrain); 11506 const elevation = terrain ? terrain.getElevationForLngLat(center, this) : 0; 11507 this._helper.recalculateZoomAndCenter(elevation); 11508 } 11509 /** 11510 * Moves the center so that `lnglat`, on the ground at `elevation` meters, renders 11511 * at the screen `point`. Both rays are cast through the same inverse pixel matrix 11512 * so its inversion error cancels out of their difference; the current-center ray 11513 * must be intersected at the center's own elevation (z=0) â intersecting it with 11514 * the elevated plane would land `(elevation - centerElevation)·tan(pitch)` away 11515 * from the center and make repeated calls drift. 11516 */ 11517 setLocationAtPoint(lnglat, point, elevation = this.elevation) { 11518 const z = elevation - this.elevation; 11519 const a = this.screenPointToMercatorCoordinateAtZ(point, z); 11520 const b = this.screenPointToMercatorCoordinateAtZ(this.centerPoint, 0); 11521 const loc = MercatorCoordinate.fromLngLat(lnglat); 11522 const newCenter = new MercatorCoordinate(loc.x - (a.x - b.x), loc.y - (a.y - b.y)); 11523 this.setCenter(newCenter?.toLngLat()); 11524 if (this._helper._renderWorldCopies) this.setCenter(this.center.wrap()); 11525 } 11526 locationToScreenPoint(lnglat, terrain) { 11527 return terrain ? this.coordinatePoint(MercatorCoordinate.fromLngLat(lnglat), terrain.getElevationForLngLat(lnglat, this), this._pixelMatrix3D) : this.coordinatePoint(MercatorCoordinate.fromLngLat(lnglat)); 11528 } 11529 screenPointToLocation(p, terrain) { 11530 return this.screenPointToMercatorCoordinate(p, terrain)?.toLngLat(); 11531 } 11532 screenPointToLocationAtElevation(p, elevation) { 11533 return this.screenPointToMercatorCoordinateAtZ(p, elevation - this.elevation)?.toLngLat(); 11534 } 11535 screenPointToMercatorCoordinate(p, terrain) { 11536 if (terrain) { 11537 const coordinate = this.screenTerrainPointToMercatorCoordinate(p, terrain); 11538 if (coordinate != null) return coordinate; 11539 } 11540 return this.screenPointToMercatorCoordinateAtZ(p); 11541 } 11542 /** {@inheritDoc ITransform.screenTerrainPointToMercatorCoordinate} */ 11543 screenTerrainPointToMercatorCoordinate(p, terrain) { 11544 const index = terrain.getCoverageIndex(); 11545 if (!index) return null; 11546 const { near, far } = this.getRaySegmentFromPixel(p); 11547 const worldSize = this.worldSize; 11548 const dx = far[0] - near[0]; 11549 const dy = far[1] - near[1]; 11550 const dz = far[2] - near[2]; 11551 const ray = { 11552 index, 11553 exaggeration: terrain.exaggeration, 11554 near, 11555 dx, 11556 dy, 11557 dz, 11558 worldSize 11559 }; 11560 let tStart = 0; 11561 let tEnd = 1; 11562 if (dz === 0) { 11563 if (near[2] > index.maxElevation || near[2] < index.minElevation) return null; 11564 } else { 11565 const tHigh = (index.maxElevation - near[2]) / dz; 11566 const tLow = (index.minElevation - near[2]) / dz; 11567 tStart = Math.max(tStart, Math.min(tHigh, tLow)); 11568 tEnd = Math.min(tEnd, Math.max(tHigh, tLow)); 11569 if (tStart > tEnd) return null; 11570 } 11571 const horizontalLength = Math.hypot(dx, dy); 11572 const samples = clamp(Math.ceil(horizontalLength * (tEnd - tStart) / TARGET_WORLD_STEP_PX), 1, MAX_SAMPLES); 11573 let previousT = 0; 11574 let aboveTerrain = !mercatorIsBelowTerrain(ray, 0); 11575 for (let i = 0; i <= samples; i++) { 11576 const t = tStart + (tEnd - tStart) * i / samples; 11577 if (!aboveTerrain) aboveTerrain = !mercatorIsBelowTerrain(ray, t); 11578 else if (mercatorIsBelowTerrain(ray, t)) { 11579 const { lo, hi } = bisect(ray, mercatorIsBelowTerrain, previousT, t, MERCATOR_BISECT_EPSILON_WORLD_PX / horizontalLength); 11580 const sampleLo = mercatorSampleAt(ray, lo); 11581 const sampleHi = mercatorSampleAt(ray, hi); 11582 const fLo = near[2] + lo * dz - sampleLo.elevation; 11583 const fHi = near[2] + hi * dz - sampleHi.elevation; 11584 const hit = sampleLo.covered && fLo > fHi ? clamp(lo + fLo * (hi - lo) / (fLo - fHi), lo, hi) : hi; 11585 return new MercatorCoordinate((near[0] + hit * dx) / worldSize, (near[1] + hit * dy) / worldSize, mercatorSampleAt(ray, hit).elevation); 11586 } 11587 previousT = t; 11588 } 11589 return null; 11590 } 11591 /** 11592 * Returns the segment of the ray through the given screen pixel that lies inside the view frustum. 11593 */ 11594 getRaySegmentFromPixel(p) { 11595 const coord0 = [ 11596 p.x, 11597 p.y, 11598 0, 11599 1 11600 ]; 11601 const coord1 = [ 11602 p.x, 11603 p.y, 11604 1, 11605 1 11606 ]; 11607 transformMat4(coord0, coord0, this._pixelMatrixInverse); 11608 transformMat4(coord1, coord1, this._pixelMatrixInverse); 11609 const w0 = coord0[3]; 11610 const w1 = coord1[3]; 11611 const elevation = this.elevation; 11612 return { 11613 near: [ 11614 coord0[0] / w0, 11615 coord0[1] / w0, 11616 coord0[2] / w0 + elevation 11617 ], 11618 far: [ 11619 coord1[0] / w1, 11620 coord1[1] / w1, 11621 coord1[2] / w1 + elevation 11622 ] 11623 }; 11624 } 11625 /** 11626 * Intersects the ray through a screen point with the horizontal plane at `z`, 11627 * given in meters relative to the plane at the center's elevation (not mercator units). 11628 */ 11629 screenPointToMercatorCoordinateAtZ(p, z) { 11630 const targetZ = z ? z : 0; 11631 const { near, far } = this.getRaySegmentFromPixel(p); 11632 const t = near[2] === far[2] ? 0 : (targetZ + this.elevation - near[2]) / (far[2] - near[2]); 11633 return new MercatorCoordinate(interpolateFactory.number(near[0], far[0], t) / this.worldSize, interpolateFactory.number(near[1], far[1], t) / this.worldSize, targetZ); 11634 } 11635 /** 11636 * Given a coordinate, return the screen point that corresponds to it 11637 * @param coord - the coordinates 11638 * @param elevation - the elevation 11639 * @param pixelMatrix - the pixel matrix 11640 * @returns screen point. Point will be outside the viewport if the coordinate is behind the camera. 11641 */ 11642 coordinatePoint(coord, elevation = 0, pixelMatrix = this._pixelMatrix) { 11643 const p = this._coordinateClipPoint(coord, elevation, pixelMatrix); 11644 const w = p[3]; 11645 if (w > 0) return new Point(p[0] / w, p[1] / w); 11646 return this._offScreenPointBehindCamera(p[0], p[1], w); 11647 } 11648 /** 11649 * The coordinate in the clip space of `pixelMatrix`: pixel x and y and NDC depth, each times `w`, which is positive 11650 * in front of the camera. 11651 */ 11652 _coordinateClipPoint(coord, elevation, pixelMatrix) { 11653 const p = [ 11654 coord.x * this.worldSize, 11655 coord.y * this.worldSize, 11656 elevation, 11657 1 11658 ]; 11659 return transformMat4(p, p, pixelMatrix); 11660 } 11661 /** 11662 * Returns a screen point outside the viewport for a coordinate that is behind the camera. 11663 * The point lies on the boundary of the viewport enlarged by one viewport size on each side, 11664 * on whichever edge the ray from the screen centre in the coordinate's direction reaches first. 11665 * A coordinate exactly behind the camera has no direction to leave through, so it is placed straight down. 11666 * @param x - the x component of the clip space position, before the division by w 11667 * @param y - the y component of the clip space position, before the division by w 11668 * @param w - the w component of the clip space position, zero or negative 11669 * @returns screen point outside the viewport 11670 */ 11671 _offScreenPointBehindCamera(x, y, w) { 11672 const cx = this.width / 2; 11673 const cy = this.height / 2; 11674 const dx = x - cx * w; 11675 let dy = y - cy * w; 11676 if (dx === 0 && dy === 0) dy = 1; 11677 const halfExtentX = cx + this.width; 11678 const halfExtentY = cy + this.height; 11679 const scale = Math.min(dx !== 0 ? halfExtentX / Math.abs(dx) : Infinity, dy !== 0 ? halfExtentY / Math.abs(dy) : Infinity); 11680 return new Point(cx + dx * scale, cy + dy * scale); 11681 } 11682 getBounds() { 11683 const top = Math.max(0, this._helper._height / 2 - getMercatorHorizon(this)); 11684 return new LngLatBounds().extend(this.screenPointToLocation(new Point(0, top))).extend(this.screenPointToLocation(new Point(this._helper._width, top))).extend(this.screenPointToLocation(new Point(this._helper._width, this._helper._height))).extend(this.screenPointToLocation(new Point(0, this._helper._height))); 11685 } 11686 isPointOnMapSurface(p, terrain) { 11687 if (terrain) return this.screenTerrainPointToMercatorCoordinate(p, terrain) != null; 11688 return p.y > this.height / 2 - getMercatorHorizon(this); 11689 } 11690 calculatePosMatrix(tileID, aligned = false, useFloat32 = false) { 11691 const posMatrixKey = tileID.key ?? calculateTileKey(tileID.wrap, tileID.canonical.z, tileID.canonical.z, tileID.canonical.x, tileID.canonical.y); 11692 const cache = aligned ? this._alignedPosMatrixCache : this._posMatrixCache; 11693 if (cache.has(posMatrixKey)) { 11694 const matrices = cache.get(posMatrixKey); 11695 return useFloat32 ? matrices.f32 : matrices.f64; 11696 } 11697 const tileMatrix = calculateTileMatrix(tileID, this.worldSize); 11698 multiply(tileMatrix, aligned ? this._alignedProjMatrix : this._viewProjMatrix, tileMatrix); 11699 const matrices = { 11700 f64: tileMatrix, 11701 f32: new Float32Array(tileMatrix) 11702 }; 11703 cache.set(posMatrixKey, matrices); 11704 return useFloat32 ? matrices.f32 : matrices.f64; 11705 } 11706 calculateFogMatrix(unwrappedTileID) { 11707 const posMatrixKey = unwrappedTileID.key; 11708 const cache = this._fogMatrixCacheF32; 11709 if (cache.has(posMatrixKey)) return cache.get(posMatrixKey); 11710 const fogMatrix = calculateTileMatrix(unwrappedTileID, this.worldSize); 11711 multiply(fogMatrix, this._fogMatrix, fogMatrix); 11712 cache.set(posMatrixKey, new Float32Array(fogMatrix)); 11713 return cache.get(posMatrixKey); 11714 } 11715 calculateCenterFromCameraLngLatAlt(lnglat, alt, bearing, pitch) { 11716 return this._helper.calculateCenterFromCameraLngLatAlt(lnglat, alt, bearing, pitch); 11717 } 11718 calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo) { 11719 const fromMercator = MercatorCoordinate.fromLngLat(from, altitudeFrom); 11720 const toMercator = MercatorCoordinate.fromLngLat(to, altitudeTo); 11721 const dx = toMercator.x - fromMercator.x; 11722 const dy = toMercator.y - fromMercator.y; 11723 const dz = toMercator.z - fromMercator.z; 11724 const distance3D = Math.hypot(dx, dy, dz); 11725 if (distance3D === 0) throw new Error("Can't calculate camera options with same From and To"); 11726 const groundDistance = Math.hypot(dx, dy); 11727 const zoom = scaleZoom(this.cameraToCenterDistance / distance3D / this.tileSize); 11728 const bearing = radiansToDegrees(Math.atan2(dx, -dy)); 11729 let pitch = radiansToDegrees(Math.acos(groundDistance / distance3D)); 11730 pitch = dz < 0 ? 90 - pitch : 90 + pitch; 11731 return { 11732 center: toMercator.toLngLat(), 11733 elevation: altitudeTo, 11734 zoom, 11735 pitch, 11736 bearing 11737 }; 11738 } 11739 _calculateNearFarZ(cameraToSeaLevelDistance, limitedPitchRadians, offset) { 11740 const minElevation = Math.min(this.elevation, this.minElevationForCurrentTile, this.getCameraAltitude() - 100); 11741 const cameraToLowestPointDistance = cameraToSeaLevelDistance - minElevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians); 11742 const lowestPlane = minElevation < 0 ? cameraToLowestPointDistance : cameraToSeaLevelDistance; 11743 const groundAngle = Math.PI / 2 + this.pitchInRadians; 11744 const fovAboveCenter = degreesToRadians(this.fov) * (Math.abs(Math.cos(degreesToRadians(this.roll))) * this.height + Math.abs(Math.sin(degreesToRadians(this.roll))) * this.width) / this.height * (.5 + offset.y / this.height); 11745 const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, .01, Math.PI - .01)); 11746 const horizon = getMercatorHorizon(this); 11747 const horizonAngle = Math.atan(horizon / this._helper.cameraToCenterDistance); 11748 const minFovCenterToHorizonRadians = degreesToRadians(90 - maxMercatorHorizonAngle); 11749 const fovCenterToHorizon = horizonAngle > minFovCenterToHorizonRadians ? 2 * horizonAngle * (.5 + offset.y / (horizon * 2)) : minFovCenterToHorizonRadians; 11750 const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, .01, Math.PI - .01)); 11751 const topHalfMinDistance = Math.min(topHalfSurfaceDistance, topHalfSurfaceDistanceHorizon); 11752 this._helper._farZ = (Math.cos(Math.PI / 2 - limitedPitchRadians) * topHalfMinDistance + lowestPlane) * 1.01; 11753 this._helper._nearZ = this._helper._height / 50; 11754 } 11755 /** 11756 * @param calculateNearFarZ - Whether to compute the near/far Z range, or leave the range the helper already 11757 * holds. Defaults to {@link autoCalculateNearFarZ}; a composing transform such as {@link GlobeTransform} 11758 * overrides it so that its two children share a single depth range. 11759 */ 11760 _calcMatrices(calculateNearFarZ = this._helper.autoCalculateNearFarZ) { 11761 const offset = this.centerOffset; 11762 const point = projectToWorldCoordinates(this.worldSize, this.center); 11763 const x = point.x, y = point.y; 11764 const limitedPitchRadians = degreesToRadians(Math.min(this.pitch, maxMercatorHorizonAngle)); 11765 const cameraToSeaLevelDistance = Math.max(this._helper.cameraToCenterDistance / 2, this._helper.cameraToCenterDistance + this._helper._elevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians)); 11766 if (calculateNearFarZ) this._calculateNearFarZ(cameraToSeaLevelDistance, limitedPitchRadians, offset); 11767 let m; 11768 m = /* @__PURE__ */ new Float64Array(16); 11769 perspective(m, this.fovInRadians, this._helper._width / this._helper._height, this._helper._nearZ, this._helper._farZ); 11770 this._invProjMatrix = /* @__PURE__ */ new Float64Array(16); 11771 fastInvertProjMat4(this._invProjMatrix, m); 11772 m[8] = -offset.x * 2 / this._helper._width; 11773 m[9] = offset.y * 2 / this._helper._height; 11774 this._projectionMatrix = clone$1(m); 11775 scale(m, m, [ 11776 1, 11777 -1, 11778 1 11779 ]); 11780 translate(m, m, [ 11781 0, 11782 0, 11783 -this._helper.cameraToCenterDistance 11784 ]); 11785 rotateZ(m, m, -this.rollInRadians); 11786 rotateX(m, m, this.pitchInRadians); 11787 rotateZ(m, m, -this.bearingInRadians); 11788 translate(m, m, [ 11789 -x, 11790 -y, 11791 0 11792 ]); 11793 this._mercatorMatrix = scale([], m, [ 11794 this.worldSize, 11795 this.worldSize, 11796 this.worldSize 11797 ]); 11798 scale(m, m, [ 11799 1, 11800 1, 11801 this._helper._pixelPerMeter 11802 ]); 11803 this._pixelMatrix = multiply(/* @__PURE__ */ new Float64Array(16), this.clipSpaceToPixelsMatrix, m); 11804 translate(m, m, [ 11805 0, 11806 0, 11807 -this.elevation 11808 ]); 11809 this._viewProjMatrix = m; 11810 this._invViewProjMatrix = invert$1([], m); 11811 const cameraPos = [ 11812 0, 11813 0, 11814 -1, 11815 1 11816 ]; 11817 transformMat4(cameraPos, cameraPos, this._invViewProjMatrix); 11818 this._cameraPosition = [ 11819 cameraPos[0] / cameraPos[3], 11820 cameraPos[1] / cameraPos[3], 11821 cameraPos[2] / cameraPos[3] 11822 ]; 11823 this._fogMatrix = /* @__PURE__ */ new Float64Array(16); 11824 perspective(this._fogMatrix, this.fovInRadians, this.width / this.height, cameraToSeaLevelDistance, this._helper._farZ); 11825 this._fogMatrix[8] = -offset.x * 2 / this.width; 11826 this._fogMatrix[9] = offset.y * 2 / this.height; 11827 scale(this._fogMatrix, this._fogMatrix, [ 11828 1, 11829 -1, 11830 1 11831 ]); 11832 translate(this._fogMatrix, this._fogMatrix, [ 11833 0, 11834 0, 11835 -this.cameraToCenterDistance 11836 ]); 11837 rotateZ(this._fogMatrix, this._fogMatrix, -this.rollInRadians); 11838 rotateX(this._fogMatrix, this._fogMatrix, this.pitchInRadians); 11839 rotateZ(this._fogMatrix, this._fogMatrix, -this.bearingInRadians); 11840 translate(this._fogMatrix, this._fogMatrix, [ 11841 -x, 11842 -y, 11843 0 11844 ]); 11845 scale(this._fogMatrix, this._fogMatrix, [ 11846 1, 11847 1, 11848 this._helper._pixelPerMeter 11849 ]); 11850 translate(this._fogMatrix, this._fogMatrix, [ 11851 0, 11852 0, 11853 -this.elevation 11854 ]); 11855 this._pixelMatrix3D = multiply(/* @__PURE__ */ new Float64Array(16), this.clipSpaceToPixelsMatrix, m); 11856 const xShift = this._helper._width % 2 / 2, yShift = this._helper._height % 2 / 2, angleCos = Math.cos(this.bearingInRadians), angleSin = Math.sin(-this.bearingInRadians), dx = x - Math.round(x) + angleCos * xShift + angleSin * yShift, dy = y - Math.round(y) + angleCos * yShift + angleSin * xShift; 11857 const alignedM = new Float64Array(m); 11858 translate(alignedM, alignedM, [ 11859 dx > .5 ? dx - 1 : dx, 11860 dy > .5 ? dy - 1 : dy, 11861 0 11862 ]); 11863 this._alignedProjMatrix = alignedM; 11864 m = invert$1(/* @__PURE__ */ new Float64Array(16), this._pixelMatrix); 11865 if (!m) throw new Error("failed to invert matrix"); 11866 this._pixelMatrixInverse = m; 11867 this._clearMatrixCaches(); 11868 } 11869 _clearMatrixCaches() { 11870 this._posMatrixCache.clear(); 11871 this._alignedPosMatrixCache.clear(); 11872 this._fogMatrixCacheF32.clear(); 11873 } 11874 maxPitchScaleFactor() { 11875 if (!this._pixelMatrixInverse) return 1; 11876 const coord = this.screenPointToMercatorCoordinate(new Point(0, 0)); 11877 const p = [ 11878 coord.x * this.worldSize, 11879 coord.y * this.worldSize, 11880 0, 11881 1 11882 ]; 11883 return transformMat4(p, p, this._pixelMatrix)[3] / this._helper.cameraToCenterDistance; 11884 } 11885 getCameraPoint() { 11886 return this._helper.getCameraPoint(); 11887 } 11888 getCameraAltitude() { 11889 return Math.cos(this.pitchInRadians) * this.cameraToCenterDistance / this.pixelsPerMeter + this.elevation; 11890 } 11891 getCameraLngLat() { 11892 const pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; 11893 const cameraToCenterDistanceMeters = this._helper.cameraToCenterDistance / pixelPerMeter; 11894 return cameraMercatorCoordinateFromCenterAndRotation(this.center, this.elevation, this.pitch, this.bearing, cameraToCenterDistanceMeters).toLngLat(); 11895 } 11896 getProjectionData(params) { 11897 const { overscaledTileID, aligned, applyTerrainMatrix } = params; 11898 const mercatorTileCoordinates = this._helper.getMercatorTileCoordinates(overscaledTileID); 11899 const tilePosMatrix = overscaledTileID ? this.calculatePosMatrix(overscaledTileID, aligned, true) : null; 11900 let mainMatrix; 11901 if (overscaledTileID?.terrainRttPosMatrix32f && applyTerrainMatrix) mainMatrix = overscaledTileID.terrainRttPosMatrix32f; 11902 else if (tilePosMatrix) mainMatrix = tilePosMatrix; 11903 else mainMatrix = createIdentityMat4f32(); 11904 return { 11905 mainMatrix, 11906 tileMercatorCoords: mercatorTileCoordinates, 11907 clippingPlane: [ 11908 0, 11909 0, 11910 0, 11911 0 11912 ], 11913 projectionTransition: 0, 11914 fallbackMatrix: mainMatrix, 11915 clipAntimeridian: false 11916 }; 11917 } 11918 /** {@inheritDoc ITransform.isLocationOccluded} */ 11919 isLocationOccluded(lngLat, terrain, elevation) { 11920 if (!terrain?.getCoverageIndex()) return false; 11921 const location = MercatorCoordinate.fromLngLat(lngLat); 11922 elevation ??= terrain.getElevationForLngLat(lngLat, this); 11923 const clip = this._coordinateClipPoint(location, elevation, this._pixelMatrix3D); 11924 const w = clip[3]; 11925 if (w <= 0 || clip[2] > w) return true; 11926 const p = new Point(clip[0] / w, clip[1] / w); 11927 const hit = this.screenTerrainPointToMercatorCoordinate(p, terrain); 11928 if (hit == null) return false; 11929 const segment = this.getRaySegmentFromPixel(p); 11930 const tLocation = raySegmentParameter(segment, location.x * this.worldSize, location.y * this.worldSize, elevation); 11931 return raySegmentParameter(segment, hit.x * this.worldSize, hit.y * this.worldSize, hit.z) < tLocation * .99; 11932 } 11933 getPixelScale() { 11934 return 1; 11935 } 11936 getCircleRadiusCorrection() { 11937 return 1; 11938 } 11939 getPitchedTextCorrection(_textAnchorX, _textAnchorY, _tileID) { 11940 return 1; 11941 } 11942 transformLightDirection(dir) { 11943 return clone(dir); 11944 } 11945 getRayDirectionFromPixel(_p) { 11946 throw new Error("Not implemented."); 11947 } 11948 projectTileCoordinates(x, y, unwrappedTileID, elevation) { 11949 const matrix = this.calculatePosMatrix(unwrappedTileID); 11950 let pos; 11951 if (elevation != null) { 11952 pos = [ 11953 x, 11954 y, 11955 elevation, 11956 1 11957 ]; 11958 transformMat4(pos, pos, matrix); 11959 } else { 11960 pos = [ 11961 x, 11962 y, 11963 0, 11964 1 11965 ]; 11966 xyTransformMat4(pos, pos, matrix); 11967 } 11968 const w = pos[3]; 11969 return { 11970 point: new Point(pos[0] / w, pos[1] / w), 11971 signedDistanceFromCamera: w, 11972 isOccluded: false 11973 }; 11974 } 11975 populateCache(coords) { 11976 for (const coord of coords) this.calculatePosMatrix(coord); 11977 } 11978 getProjectionDataForCustomLayer(applyGlobeMatrix = true) { 11979 const tileID = new OverscaledTileID(0, 0, 0, 0, 0); 11980 const rendererProjectionData = this.getProjectionData({ 11981 overscaledTileID: tileID, 11982 applyGlobeMatrix 11983 }); 11984 const tileMatrix = calculateTileMatrix(tileID, this.worldSize); 11985 multiply(tileMatrix, this._viewProjMatrix, tileMatrix); 11986 const scale$4 = [ 11987 EXTENT, 11988 EXTENT, 11989 this.worldSize / this._helper.pixelsPerMeter 11990 ]; 11991 const projectionMatrixScaled = createMat4f64(); 11992 scale(projectionMatrixScaled, tileMatrix, scale$4); 11993 return { 11994 ...rendererProjectionData, 11995 tileMercatorCoords: [ 11996 0, 11997 0, 11998 1, 11999 1 12000 ], 12001 fallbackMatrix: projectionMatrixScaled, 12002 mainMatrix: projectionMatrixScaled 12003 }; 12004 } 12005 getFastPathSimpleProjectionMatrix(tileID) { 12006 return this.calculatePosMatrix(tileID); 12007 } 12008}; 12009function mercatorSampleAt(ray, t) { 12010 return sampleAt(ray.index, ray.exaggeration, (ray.near[0] + t * ray.dx) / ray.worldSize, (ray.near[1] + t * ray.dy) / ray.worldSize); 12011} 12012function mercatorIsBelowTerrain(ray, t) { 12013 return isBelowTerrainSample(mercatorSampleAt(ray, t), ray.near[2] + t * ray.dz); 12014} 12015/** 12016* Where the point of `segment` closest to the given world pixel position and elevation lies along it, 12017* as the fraction from `near` (0) to `far` (1). 12018*/ 12019function raySegmentParameter({ near, far }, worldX, worldY, elevation) { 12020 const dx = far[0] - near[0]; 12021 const dy = far[1] - near[1]; 12022 const dz = far[2] - near[2]; 12023 return ((worldX - near[0]) * dx + (worldY - near[1]) * dy + (elevation - near[2]) * dz) / (dx * dx + dy * dy + dz * dz); 12024} 12025//#endregion 12026//#region src/geo/projection/camera_helper.ts 12027/** 12028* @internal 12029*/ 12030function cameraBoundsWarning() { 12031 warnOnce("Map cannot fit within canvas with the given bounds, padding, and/or offset."); 12032} 12033/** 12034* @internal 12035* Set a transform's rotation to a value interpolated between startEulerAngles and endEulerAngles 12036*/ 12037function updateRotation(args) { 12038 if (args.useSlerp) { 12039 if (args.k < 1) { 12040 const startRotation = rollPitchBearingToQuat(args.startEulerAngles.roll, args.startEulerAngles.pitch, args.startEulerAngles.bearing); 12041 const endRotation = rollPitchBearingToQuat(args.endEulerAngles.roll, args.endEulerAngles.pitch, args.endEulerAngles.bearing); 12042 const rotation = /* @__PURE__ */ new Float64Array(4); 12043 slerp(rotation, startRotation, endRotation, args.k); 12044 const eulerAngles = getRollPitchBearing(rotation); 12045 args.tr.setRoll(eulerAngles.roll); 12046 args.tr.setPitch(eulerAngles.pitch); 12047 args.tr.setBearing(eulerAngles.bearing); 12048 } else { 12049 args.tr.setRoll(args.endEulerAngles.roll); 12050 args.tr.setPitch(args.endEulerAngles.pitch); 12051 args.tr.setBearing(args.endEulerAngles.bearing); 12052 } 12053 } else { 12054 args.tr.setRoll(interpolateFactory.number(args.startEulerAngles.roll, args.endEulerAngles.roll, args.k)); 12055 args.tr.setPitch(interpolateFactory.number(args.startEulerAngles.pitch, args.endEulerAngles.pitch, args.k)); 12056 args.tr.setBearing(interpolateFactory.number(args.startEulerAngles.bearing, args.endEulerAngles.bearing, args.k)); 12057 } 12058} 12059function cameraForBoxAndBearing(options, padding, bounds, bearing, tr) { 12060 const edgePadding = tr.padding; 12061 const nwWorld = projectToWorldCoordinates(tr.worldSize, bounds.getNorthWest()); 12062 const neWorld = projectToWorldCoordinates(tr.worldSize, bounds.getNorthEast()); 12063 const seWorld = projectToWorldCoordinates(tr.worldSize, bounds.getSouthEast()); 12064 const swWorld = projectToWorldCoordinates(tr.worldSize, bounds.getSouthWest()); 12065 const bearingRadians = degreesToRadians(-bearing); 12066 const nwRotatedWorld = nwWorld.rotate(bearingRadians); 12067 const neRotatedWorld = neWorld.rotate(bearingRadians); 12068 const seRotatedWorld = seWorld.rotate(bearingRadians); 12069 const swRotatedWorld = swWorld.rotate(bearingRadians); 12070 const upperRight = new Point(Math.max(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x), Math.max(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)); 12071 const lowerLeft = new Point(Math.min(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x), Math.min(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)); 12072 const size = upperRight.sub(lowerLeft); 12073 const availableWidth = tr.width - (edgePadding.left + edgePadding.right + padding.left + padding.right); 12074 const availableHeight = tr.height - (edgePadding.top + edgePadding.bottom + padding.top + padding.bottom); 12075 const scaleX = availableWidth / size.x; 12076 const scaleY = availableHeight / size.y; 12077 if (scaleY < 0 || scaleX < 0) { 12078 cameraBoundsWarning(); 12079 return; 12080 } 12081 const zoom = Math.min(scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom); 12082 const offset = Point.convert(options.offset); 12083 const paddingOffsetX = (padding.left - padding.right) / 2; 12084 const paddingOffsetY = (padding.top - padding.bottom) / 2; 12085 const rotatedPaddingOffset = new Point(paddingOffsetX, paddingOffsetY).rotate(degreesToRadians(bearing)); 12086 const offsetAtFinalZoom = offset.add(rotatedPaddingOffset).mult(tr.scale / zoomScale(zoom)); 12087 return { 12088 center: unprojectFromWorldCoordinates(tr.worldSize, nwWorld.add(seWorld).div(2).sub(offsetAtFinalZoom)), 12089 zoom, 12090 bearing 12091 }; 12092} 12093//#endregion 12094//#region src/geo/projection/mercator_camera_helper.ts 12095/** 12096* @internal 12097*/ 12098var MercatorCameraHelper = class { 12099 get useGlobeControls() { 12100 return false; 12101 } 12102 handlePanInertia(pan, transform) { 12103 const offsetLength = pan.mag(); 12104 const pixelsToHorizon = Math.abs(getMercatorHorizon(transform)); 12105 return { 12106 easingOffset: pan.mult(Math.min(pixelsToHorizon * .75 / offsetLength, 1)), 12107 easingCenter: transform.center 12108 }; 12109 } 12110 handleMapControlsRollPitchBearingZoom(deltas, tr) { 12111 if (deltas.bearingDelta) tr.setBearing(tr.bearing + deltas.bearingDelta); 12112 if (deltas.pitchDelta) tr.setPitch(tr.pitch + deltas.pitchDelta); 12113 if (deltas.rollDelta) tr.setRoll(tr.roll + deltas.rollDelta); 12114 if (deltas.zoomDelta) tr.setZoom(tr.zoom + deltas.zoomDelta); 12115 } 12116 handleMapControlsPan(deltas, tr, preZoomAroundLoc) { 12117 if (deltas.around.distSqr(tr.centerPoint) < .01) return; 12118 tr.setLocationAtPoint(preZoomAroundLoc, deltas.around, deltas.aroundElevation); 12119 } 12120 cameraForBoxAndBearing(options, padding, bounds, bearing, tr) { 12121 return cameraForBoxAndBearing(options, padding, bounds, bearing, tr); 12122 } 12123 handleJumpToCenterZoom(tr, options) { 12124 const zoom = typeof options.zoom !== "undefined" ? +options.zoom : tr.zoom; 12125 if (tr.zoom !== zoom) tr.setZoom(+options.zoom); 12126 if (options.center !== void 0) tr.setCenter(LngLat.convert(options.center)); 12127 } 12128 handleEaseTo(tr, options) { 12129 const startZoom = tr.zoom; 12130 const startPadding = tr.padding; 12131 const startEulerAngles = { 12132 roll: tr.roll, 12133 pitch: tr.pitch, 12134 bearing: tr.bearing 12135 }; 12136 const endEulerAngles = { 12137 roll: options.roll === void 0 ? tr.roll : options.roll, 12138 pitch: options.pitch === void 0 ? tr.pitch : options.pitch, 12139 bearing: options.bearing === void 0 ? tr.bearing : options.bearing 12140 }; 12141 const optionsZoom = typeof options.zoom !== "undefined"; 12142 const doPadding = !tr.isPaddingEqual(options.padding); 12143 let isZooming = false; 12144 const zoom = optionsZoom ? +options.zoom : tr.zoom; 12145 let pointAtOffset = tr.centerPoint.add(options.offsetAsPoint); 12146 const locationAtOffset = tr.screenPointToLocation(pointAtOffset); 12147 const { center, zoom: endZoom } = tr.applyConstrain(LngLat.convert(options.center || locationAtOffset), zoom ?? startZoom); 12148 normalizeCenter(tr, center); 12149 const from = projectToWorldCoordinates(tr.worldSize, locationAtOffset); 12150 const delta = projectToWorldCoordinates(tr.worldSize, center).sub(from); 12151 const finalScale = zoomScale(endZoom - startZoom); 12152 isZooming = endZoom !== startZoom; 12153 const easeFunc = (k) => { 12154 if (isZooming) tr.setZoom(interpolateFactory.number(startZoom, endZoom, k)); 12155 if (!rollPitchBearingEqual(startEulerAngles, endEulerAngles)) updateRotation({ 12156 startEulerAngles, 12157 endEulerAngles, 12158 tr, 12159 k, 12160 useSlerp: startEulerAngles.roll != endEulerAngles.roll 12161 }); 12162 if (doPadding) { 12163 tr.interpolatePadding(startPadding, options.padding, k); 12164 pointAtOffset = tr.centerPoint.add(options.offsetAsPoint); 12165 } 12166 if (options.around) tr.setLocationAtPoint(options.around, options.aroundPoint); 12167 else { 12168 const scale = zoomScale(tr.zoom - startZoom); 12169 const speedup = Math.pow(endZoom > startZoom ? Math.min(2, finalScale) : Math.max(.5, finalScale), 1 - k); 12170 const newCenter = unprojectFromWorldCoordinates(tr.worldSize, from.add(delta.mult(k * speedup)).mult(scale)); 12171 tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); 12172 } 12173 }; 12174 return { 12175 easeFunc, 12176 isZooming, 12177 elevationCenter: center 12178 }; 12179 } 12180 handleFlyTo(tr, options) { 12181 const optionsZoom = typeof options.zoom !== "undefined"; 12182 const startZoom = tr.zoom; 12183 const constrained = tr.applyConstrain(LngLat.convert(options.center || options.locationAtOffset), optionsZoom ? +options.zoom : startZoom); 12184 const targetCenter = constrained.center; 12185 const targetZoom = constrained.zoom; 12186 normalizeCenter(tr, targetCenter); 12187 const startWorldSize = tr.worldSize; 12188 const from = projectToWorldCoordinates(startWorldSize, options.locationAtOffset); 12189 const delta = projectToWorldCoordinates(startWorldSize, targetCenter).sub(from); 12190 const pixelPathLength = delta.mag(); 12191 const scaleOfZoom = zoomScale(targetZoom - startZoom); 12192 const requestedMinZoom = typeof options.minZoom !== "undefined" ? +options.minZoom : tr.minZoom; 12193 const effectiveMinZoom = Math.max(requestedMinZoom, tr.minZoom); 12194 const minZoomPreConstrain = Math.min(effectiveMinZoom, startZoom, targetZoom); 12195 const minZoom = tr.applyConstrain(targetCenter, minZoomPreConstrain).zoom; 12196 const scaleOfMinZoom = zoomScale(minZoom - startZoom); 12197 const easeFunc = (k, scale, centerFactor, pointAtOffset) => { 12198 tr.setZoom(k === 1 ? targetZoom : startZoom + scaleZoom(scale)); 12199 const newCenter = k === 1 ? targetCenter : unprojectFromWorldCoordinates(startWorldSize, from.add(delta.mult(centerFactor))); 12200 tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); 12201 }; 12202 return { 12203 easeFunc, 12204 scaleOfZoom, 12205 targetCenter, 12206 scaleOfMinZoom, 12207 pixelPathLength 12208 }; 12209 } 12210}; 12211//#endregion 12212//#region src/style/projection_properties.g.ts 12213let properties; 12214const getProperties = () => properties = properties || new Properties({ "type": new DataConstantProperty(latest["projection"]["type"], "type") }); 12215//#endregion 12216//#region src/geo/projection/vertical_perspective_projection.ts 12217const VerticalPerspectiveShaderDefine = "#define GLOBE"; 12218const VerticalPerspectiveShaderVariantKey = "globe"; 12219const granularitySettingsGlobe = new SubdivisionGranularitySetting({ 12220 fill: new SubdivisionGranularityExpression(128, 2), 12221 line: new SubdivisionGranularityExpression(512, 0), 12222 tile: new SubdivisionGranularityExpression(128, 32), 12223 stencil: new SubdivisionGranularityExpression(128, 1), 12224 circle: 3 12225}); 12226var VerticalPerspectiveProjection = class { 12227 constructor() { 12228 this._tileMeshCache = {}; 12229 } 12230 get name() { 12231 return "vertical-perspective"; 12232 } 12233 get transitionState() { 12234 return 1; 12235 } 12236 get useSubdivision() { 12237 return true; 12238 } 12239 get shaderVariantName() { 12240 return VerticalPerspectiveShaderVariantKey; 12241 } 12242 get shaderDefine() { 12243 return VerticalPerspectiveShaderDefine; 12244 } 12245 get shaderPreludeCode() { 12246 return shaders.projectionGlobe; 12247 } 12248 get vertexShaderPreludeCode() { 12249 return shaders.projectionMercator.vertexSource; 12250 } 12251 get subdivisionGranularity() { 12252 return granularitySettingsGlobe; 12253 } 12254 get useGlobeControls() { 12255 return true; 12256 } 12257 destroy() {} 12258 _getMeshKey(options) { 12259 return `${options.granularity.toString(36)}_${options.generateBorders ? "b" : ""}${options.extendToNorthPole ? "n" : ""}${options.extendToSouthPole ? "s" : ""}`; 12260 } 12261 getMeshFromTileID(context, canonical, hasBorder, allowPoles, usage) { 12262 const granularity = (usage === "stencil" ? granularitySettingsGlobe.stencil : granularitySettingsGlobe.tile).getGranularityForZoomLevel(canonical.z); 12263 const north = canonical.y === 0 && allowPoles; 12264 const south = canonical.y === (1 << canonical.z) - 1 && allowPoles; 12265 return this._getMesh(context, { 12266 granularity, 12267 generateBorders: hasBorder, 12268 extendToNorthPole: north, 12269 extendToSouthPole: south 12270 }); 12271 } 12272 _getMesh(context, options) { 12273 const key = this._getMeshKey(options); 12274 if (key in this._tileMeshCache) return this._tileMeshCache[key]; 12275 const mesh = createTileMeshWithBuffers(context, options); 12276 this._tileMeshCache[key] = mesh; 12277 return mesh; 12278 } 12279 recalculate(_params) {} 12280 hasTransition() { 12281 return false; 12282 } 12283}; 12284//#endregion 12285//#region src/geo/projection/globe_projection.ts 12286var GlobeProjection = class extends Evented { 12287 constructor(projection, globalState) { 12288 super(); 12289 this._transitionable = new Transitionable(getProperties(), "projection", globalState); 12290 this.setProjection(projection); 12291 this._transitioning = this._transitionable.untransitioned(); 12292 this.recalculate(new EvaluationParameters(0)); 12293 this._mercatorProjection = new MercatorProjection(); 12294 this._verticalPerspectiveProjection = new VerticalPerspectiveProjection(); 12295 } 12296 get transitionState() { 12297 const currentProjectionSpecValue = this.properties.get("type"); 12298 if (typeof currentProjectionSpecValue === "string" && currentProjectionSpecValue === "mercator") return 0; 12299 if (typeof currentProjectionSpecValue === "string" && currentProjectionSpecValue === "vertical-perspective") return 1; 12300 if (currentProjectionSpecValue instanceof ProjectionDefinition) { 12301 if (currentProjectionSpecValue.from === currentProjectionSpecValue.to) return currentProjectionSpecValue.from === "mercator" ? 0 : 1; 12302 if (currentProjectionSpecValue.from === "vertical-perspective" && currentProjectionSpecValue.to === "mercator") return 1 - currentProjectionSpecValue.transition; 12303 if (currentProjectionSpecValue.from === "mercator" && currentProjectionSpecValue.to === "vertical-perspective") return currentProjectionSpecValue.transition; 12304 } 12305 return 1; 12306 } 12307 get useGlobeRendering() { 12308 return this.transitionState > 0; 12309 } 12310 get currentProjection() { 12311 return this.useGlobeRendering ? this._verticalPerspectiveProjection : this._mercatorProjection; 12312 } 12313 get name() { 12314 return "globe"; 12315 } 12316 get useSubdivision() { 12317 return this.currentProjection.useSubdivision; 12318 } 12319 get shaderVariantName() { 12320 return this.currentProjection.shaderVariantName; 12321 } 12322 get shaderDefine() { 12323 return this.currentProjection.shaderDefine; 12324 } 12325 get shaderPreludeCode() { 12326 return this.currentProjection.shaderPreludeCode; 12327 } 12328 get vertexShaderPreludeCode() { 12329 return this.currentProjection.vertexShaderPreludeCode; 12330 } 12331 get subdivisionGranularity() { 12332 return this.currentProjection.subdivisionGranularity; 12333 } 12334 get useGlobeControls() { 12335 return this.transitionState > 0; 12336 } 12337 destroy() { 12338 this._mercatorProjection.destroy(); 12339 this._verticalPerspectiveProjection.destroy(); 12340 } 12341 getMeshFromTileID(context, _tileID, _hasBorder, _allowPoles, _usage) { 12342 return this.currentProjection.getMeshFromTileID(context, _tileID, _hasBorder, _allowPoles, _usage); 12343 } 12344 setProjection(projection) { 12345 this._transitionable.setValue("type", projection?.type || "mercator"); 12346 } 12347 updateTransitions(parameters) { 12348 this._transitioning = this._transitionable.transitioned(parameters, this._transitioning); 12349 } 12350 hasTransition() { 12351 return this._transitioning.hasTransition() || this.currentProjection.hasTransition(); 12352 } 12353 recalculate(parameters) { 12354 this.properties = this._transitioning.possiblyEvaluate(parameters); 12355 } 12356}; 12357//#endregion 12358//#region src/geo/projection/globe_utils.ts 12359function getGlobeCircumferencePixels(transform) { 12360 const radius = getGlobeRadiusPixels(transform.worldSize, transform.center.lat); 12361 return 2 * Math.PI * radius; 12362} 12363function globeDistanceOfLocationsPixels(transform, a, b) { 12364 const vecA = angularCoordinatesToSurfaceVector(a); 12365 const vecB = angularCoordinatesToSurfaceVector(b); 12366 const dot$2 = dot(vecA, vecB); 12367 const radians = Math.acos(dot$2); 12368 const circumference = getGlobeCircumferencePixels(transform); 12369 return radians / (2 * Math.PI) * circumference; 12370} 12371/** 12372* For given mercator coordinates in range 0..1, returns the angular coordinates on the sphere's surface, in radians. 12373*/ 12374function mercatorCoordinatesToAngularCoordinatesRadians(mercatorX, mercatorY) { 12375 return [mod(mercatorX * Math.PI * 2 + Math.PI, Math.PI * 2), 2 * Math.atan(Math.exp(Math.PI - mercatorY * Math.PI * 2)) - Math.PI * .5]; 12376} 12377/** 12378* For a given longitude and latitude (note: in radians) returns the normalized vector from the planet center to the specified place on the surface. 12379* @param lngRadians - Longitude in radians. 12380* @param latRadians - Latitude in radians. 12381*/ 12382function angularCoordinatesRadiansToVector(lngRadians, latRadians) { 12383 const len = Math.cos(latRadians); 12384 const vec = /* @__PURE__ */ new Float64Array(3); 12385 vec[0] = Math.sin(lngRadians) * len; 12386 vec[1] = Math.sin(latRadians); 12387 vec[2] = Math.cos(lngRadians) * len; 12388 return vec; 12389} 12390/** 12391* Projects a point within a tile to the surface of the unit sphere globe. 12392* @param inTileX - X coordinate inside the tile in range [0 .. 8192]. 12393* @param inTileY - Y coordinate inside the tile in range [0 .. 8192]. 12394* @param tileIdX - Tile's X coordinate in range [0 .. 2^zoom - 1]. 12395* @param tileIdY - Tile's Y coordinate in range [0 .. 2^zoom - 1]. 12396* @param tileIdZ - Tile's zoom. 12397* @returns A 3D vector - coordinates of the projected point on a unit sphere. 12398*/ 12399function projectTileCoordinatesToSphere(inTileX, inTileY, tileIdX, tileIdY, tileIdZ) { 12400 const scale = 1 / (1 << tileIdZ); 12401 const mercatorX = inTileX / EXTENT * scale + tileIdX * scale; 12402 const mercatorY = inTileY / EXTENT * scale + tileIdY * scale; 12403 const sphericalX = mod(mercatorX * Math.PI * 2 + Math.PI, Math.PI * 2); 12404 const sphericalY = 2 * Math.atan(Math.exp(Math.PI - mercatorY * Math.PI * 2)) - Math.PI * .5; 12405 const len = Math.cos(sphericalY); 12406 const vec = /* @__PURE__ */ new Float64Array(3); 12407 vec[0] = Math.sin(sphericalX) * len; 12408 vec[1] = Math.sin(sphericalY); 12409 vec[2] = Math.cos(sphericalX) * len; 12410 return vec; 12411} 12412/** 12413* For a given longitude and latitude (note: in degrees) returns the normalized vector from the planet center to the specified place on the surface. 12414*/ 12415function angularCoordinatesToSurfaceVector(lngLat) { 12416 return angularCoordinatesRadiansToVector(lngLat.lng * Math.PI / 180, lngLat.lat * Math.PI / 180); 12417} 12418function getGlobeRadiusPixels(worldSize, latitudeDegrees) { 12419 return worldSize / (2 * Math.PI) / Math.cos(latitudeDegrees * Math.PI / 180); 12420} 12421/** 12422* Returns the globe center in view space, in pixels: the camera is at the origin and looks down the negative Z axis. 12423*/ 12424function getGlobeCenterInViewSpace(transform) { 12425 const position = transformMat4(createVec4f64(), [ 12426 0, 12427 0, 12428 0, 12429 1 12430 ], transform.modelViewProjectionMatrix); 12431 transformMat4(position, position, transform.inverseProjectionMatrix); 12432 return [ 12433 position[0] / position[3], 12434 position[1] / position[3], 12435 position[2] / position[3] 12436 ]; 12437} 12438/** 12439* Given a 3D point on the surface of a unit sphere, returns its angular coordinates in degrees. 12440* The input vector must be normalized. 12441*/ 12442function sphereSurfacePointToCoordinates(surface) { 12443 const latDegrees = Math.asin(surface[1]) / Math.PI * 180; 12444 const lengthXZ = Math.sqrt(surface[0] * surface[0] + surface[2] * surface[2]); 12445 if (lengthXZ > 1e-6) { 12446 const projX = surface[0] / lengthXZ; 12447 const projZ = surface[2] / lengthXZ; 12448 const acosZ = Math.acos(projZ); 12449 const lngDegrees = (projX > 0 ? acosZ : -acosZ) / Math.PI * 180; 12450 return new LngLat(wrap(lngDegrees, -180, 180), latDegrees); 12451 } else return new LngLat(0, latDegrees); 12452} 12453/** 12454* Returns the globe orientation quaternion for the given map center and bearing. 12455* The inverse of {@link lngLatBearingFromOrientation}. 12456*/ 12457function orientationFromLngLatBearing(lngLat, bearing) { 12458 return fromEuler(createVec4f64(), -lngLat.lng, -lngLat.lat, bearing); 12459} 12460/** 12461* Given a globe orientation quaternion, returns the corresponding map center and bearing. 12462* The inverse of {@link orientationFromLngLatBearing}. 12463*/ 12464function lngLatBearingFromOrientation(q) { 12465 const x = q[0], y = q[1], z = q[2], w = q[3]; 12466 return { 12467 lng: -Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)) * 180 / Math.PI, 12468 lat: -Math.asin(clamp(2 * (w * y - z * x), -1, 1)) * 180 / Math.PI, 12469 bearing: Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)) * 180 / Math.PI 12470 }; 12471} 12472/** Kept just below a half turn so the center cannot land exactly on the far side of the globe. */ 12473const PAN_MAX_ANGLE = Math.PI * .98; 12474/** 12475* Returns the point on the globe that a drag towards `point` should aim at. 12476* 12477* Exact tracking breaks down at the silhouette: the ray meets the globe at `asin(D sin a) - a`, 12478* whose slope runs away to infinity as the ray goes tangent, and past it there is no intersection 12479* at all. So the exact curve is left {@link PAN_FALLOFF_BAND} early and continued with a hyperbola 12480* matching it in value and slope, easing off and saturating short of the far side. Bell's virtual 12481* trackball takes the same idea of easing a sphere into a hyperbola before the rim, though it fits 12482* the curve differently: SGI's `trackball.c`, described in Henriksen, Sporring and Hornbæk, 12483* "Virtual Trackballs Revisited", IEEE TVCG 10(2):206-216, 2004. 12484* 12485* The angle comes from atan2, so a ray pointing away from the globe is a wide angle the falloff 12486* saturates rather than a case to reject. 12487* @param tr - The transform being dragged. 12488* @param point - The cursor position. 12489*/ 12490function panSurfaceLocation(tr, point) { 12491 const origin = tr.cameraPosition; 12492 const distance = length(origin); 12493 if (distance <= 1) return tr.screenPointToLocation(point); 12494 const u = createVec3f64(); 12495 normalize(u, origin); 12496 const direction = tr.getRayDirectionFromPixel(point); 12497 const c = -dot(direction, u); 12498 const lateral = createVec3f64(); 12499 scaleAndAdd(lateral, direction, u, c); 12500 const s = length(lateral); 12501 if (s < 1e-9) return tr.screenPointToLocation(point); 12502 const angle = Math.atan2(s, c); 12503 const handoverAngle = Math.asin(1 / distance) * .9; 12504 if (angle < handoverAngle) return tr.screenPointToLocation(point); 12505 const sinHandover = distance * Math.sin(handoverAngle); 12506 const targetAtHandover = Math.asin(clamp(sinHandover, -1, 1)) - handoverAngle; 12507 const slopeAtHandover = distance * Math.cos(handoverAngle) / Math.sqrt(Math.max(1 - sinHandover * sinHandover, 1e-12)) - 1; 12508 const room = PAN_MAX_ANGLE - targetAtHandover; 12509 const excess = angle - handoverAngle; 12510 const target = targetAtHandover + room * (slopeAtHandover * excess) / (room + slopeAtHandover * excess); 12511 const e = createVec3f64(); 12512 scale$1(e, lateral, 1 / s); 12513 const surface = createVec3f64(); 12514 scale$1(surface, u, Math.cos(clamp(target, 0, PAN_MAX_ANGLE))); 12515 scaleAndAdd(surface, surface, e, Math.sin(clamp(target, 0, PAN_MAX_ANGLE))); 12516 normalize(surface, surface); 12517 return sphereSurfacePointToCoordinates(surface); 12518} 12519/** 12520* Rotates the globe so that the given location appears at the given screen point, by composing 12521* versors, the unit quaternions that represent rotations. Unlike the bearing-preserving 12522* {@link ITransform.setLocationAtPoint}, this stays smooth near and across the poles, and keeps 12523* panning once the cursor leaves the globe. 12524* 12525* Note: the delta rotation's axis is in the surface-vector frame of 12526* {@link angularCoordinatesToSurfaceVector}, while the orientation quaternion uses the Euler frame 12527* of {@link orientationFromLngLatBearing}, hence the component swizzle where they combine. Zoom is 12528* adjusted to keep the planet the same size, as `setLocationAtPoint` does. 12529* @param tr - The transform to rotate. 12530* @param lnglat - The location to bring under `point`. 12531* @param point - The screen point that `lnglat` should appear at. 12532* @param panDelta - The drag's pixel delta. Used to re-derive the previous cursor location through 12533* {@link panSurfaceLocation}, since both ends of the rotation must come from the same mapping. 12534* @param fixedBearing - Applies the swing only, keeping the bearing fixed, which is what dragging 12535* the globe does. Pass `false` to apply the twist about the view axis as well, so that the grabbed 12536* location tracks the cursor exactly and the bearing drifts with it. 12537*/ 12538function versorSetLocationAtPoint(tr, lnglat, point, panDelta, fixedBearing = true) { 12539 const pointLngLat = panSurfaceLocation(tr, point); 12540 let sourceLngLat = lnglat; 12541 if (panDelta) sourceLngLat = panSurfaceLocation(tr, point.sub(panDelta)); 12542 else if (!tr.isPointOnMapSurface(point)) return; 12543 const vecToPixelCurrent = angularCoordinatesToSurfaceVector(pointLngLat); 12544 const vecToTarget = angularCoordinatesToSurfaceVector(sourceLngLat); 12545 const centerQuat = orientationFromLngLatBearing(tr.center, tr.bearing); 12546 const w = cross(createVec3f64(), vecToTarget, vecToPixelCurrent); 12547 const l = Math.sqrt(dot(w, w)); 12548 const t = Math.acos(clamp(dot(vecToTarget, vecToPixelCurrent), -1, 1)) / 2; 12549 const s = Math.sin(t); 12550 const delta = l ? fromValues(w[1] / l * s, -w[0] / l * s, w[2] / l * s, Math.cos(t)) : fromValues(0, 0, 0, 1); 12551 const { lng: newCenterLng, lat: newCenterLat, bearing: newBearing } = lngLatBearingFromOrientation(multiply$1(createVec4f64(), centerQuat, delta)); 12552 const oldLat = tr.center.lat; 12553 const oldZoom = tr.zoom; 12554 const finalLat = clamp(newCenterLat, -90, 90); 12555 const finalLng = fixedBearing ? fixedBearingLongitude(tr, point, panDelta, newCenterLng) : newCenterLng; 12556 tr.setCenter(new LngLat(wrap(finalLng, -180, 180), finalLat)); 12557 if (!fixedBearing) tr.setBearing(newBearing); 12558 tr.setZoom(oldZoom + getZoomAdjustment(oldLat, tr.center.lat)); 12559} 12560/** 12561* Returns the center longitude for a bearing-preserving drag. 12562* 12563* The swing longitude becomes ill-conditioned near the pole and the grabbed location slips away 12564* from the cursor, so within the last ~12 degrees of latitude the cursor is treated as turning a 12565* dial around the pole, blended in with a smoothstep anchored on {@link MAX_VALID_LATITUDE}, the 12566* highest latitude the center can reach. The sweep comes from the raw pixel delta rather than a 12567* round-tripped previous cursor position, which would lose its sign to cancellation at the pole, 12568* and is damped within {@link DIAL_MIN_RADIUS_PIXELS} so it eases to nothing there instead of 12569* being dropped, which would leave a spot where the drag could not move at all. 12570* @param tr - The transform being dragged. 12571* @param point - The cursor position. 12572* @param panDelta - The drag's pixel delta, or undefined to use the swing longitude only. 12573* @param newCenterLng - The swing target longitude. 12574* @returns the center longitude to apply. 12575*/ 12576function fixedBearingLongitude(tr, point, panDelta, newCenterLng) { 12577 const oldLng = tr.center.lng; 12578 const oldLat = tr.center.lat; 12579 const poleLat = oldLat >= 0 ? 90 : -90; 12580 const polePoint = tr.locationToScreenPoint(new LngLat(0, poleLat)); 12581 const rx = point.x - polePoint.x, ry = point.y - polePoint.y; 12582 const r2 = rx * rx + ry * ry; 12583 const tRamp = clamp(1 - (MAX_VALID_LATITUDE - Math.abs(oldLat)) / 12, 0, 1); 12584 const dial = tRamp * tRamp * (3 - 2 * tRamp); 12585 const dLngSwing = mod(newCenterLng - oldLng + 180, 360) - 180; 12586 let dLngDial = 0; 12587 if (dial > 0 && panDelta) { 12588 const dTheta = (rx * panDelta.y - ry * panDelta.x) / Math.max(r2, 400); 12589 dLngDial = (poleLat > 0 ? 1 : -1) * dTheta * 180 / Math.PI; 12590 } 12591 return oldLng + (1 - dial) * dLngSwing + dial * dLngDial; 12592} 12593/** 12594* Given a normalized horizon plane in Ax+By+Cz+D=0 format, compute the center and radius of 12595* the circle in that plain that contains the entire visible portion of the unit sphere from horizon 12596* to horizon. 12597* @param horizonPlane - The plane that passes through visible horizon in Ax + By + Cz + D = 0 format where mag(A,B,C)=1 12598* @returns the center point and radius of the disc that passes through the entire visible horizon 12599*/ 12600function horizonPlaneToCenterAndRadius(horizonPlane) { 12601 const center = createVec3f64(); 12602 center[0] = horizonPlane[0] * -horizonPlane[3]; 12603 center[1] = horizonPlane[1] * -horizonPlane[3]; 12604 center[2] = horizonPlane[2] * -horizonPlane[3]; 12605 return { 12606 center, 12607 radius: Math.sqrt(1 - horizonPlane[3] * horizonPlane[3]) 12608 }; 12609} 12610/** 12611* Computes the closest point on a sphere to `point`. 12612* @param center - Center of the sphere 12613* @param radius - Radius of the sphere 12614* @param point - Point inside or outside the sphere 12615* @returns A 3d vector of the point on the sphere closest to `point` 12616*/ 12617function clampToSphere(center, radius, point) { 12618 const relativeToCenter = createVec3f64(); 12619 sub(relativeToCenter, point, center); 12620 const clamped = createVec3f64(); 12621 scaleAndAdd(clamped, center, relativeToCenter, radius / len(relativeToCenter)); 12622 return clamped; 12623} 12624function planetScaleAtLatitude(latitudeDegrees) { 12625 return Math.cos(latitudeDegrees * Math.PI / 180); 12626} 12627/** 12628* Computes how much to modify zoom to keep the globe size constant when changing latitude. 12629* @param transform - An instance of any transform. Does not have any relation on the computed values. 12630* @param oldLat - Latitude before change, in degrees. 12631* @param newLat - Latitude after change, in degrees. 12632* @returns A value to add to zoom level used for old latitude to keep same planet radius at new latitude. 12633*/ 12634function getZoomAdjustment(oldLat, newLat) { 12635 const oldCircumference = planetScaleAtLatitude(oldLat); 12636 const newCircumference = planetScaleAtLatitude(newLat); 12637 return scaleZoom(newCircumference / oldCircumference); 12638} 12639function getDegreesPerPixel(worldSize, lat) { 12640 return 360 / getGlobeCircumferencePixels({ 12641 worldSize, 12642 center: { lat } 12643 }); 12644} 12645/** 12646* Returns transform's new center rotation after applying panning. 12647* @param panDelta - Panning delta, in same units as what is supplied to {@link HandlerManager}. 12648* @param tr - Current transform. This object is not modified by the function. 12649* @returns New center location to set to the map's transform to apply the specified panning. 12650*/ 12651function computeGlobePanCenter(panDelta, tr) { 12652 const rotatedPanDelta = panDelta.rotate(tr.bearingInRadians); 12653 const normalizedGlobeZoom = tr.zoom + getZoomAdjustment(tr.center.lat, 0); 12654 const lngSpeed = lerp(1 / planetScaleAtLatitude(tr.center.lat), 1 / planetScaleAtLatitude(Math.min(Math.abs(tr.center.lat), 60)), remapSaturate(normalizedGlobeZoom, 7, 3, 0, 1)); 12655 const panningDegreesPerPixel = getDegreesPerPixel(tr.worldSize, tr.center.lat); 12656 return new LngLat(tr.center.lng - rotatedPanDelta.x * panningDegreesPerPixel * lngSpeed, clamp(tr.center.lat + rotatedPanDelta.y * panningDegreesPerPixel, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE)); 12657} 12658/** 12659* Integration of `1 / cos(x)`. 12660*/ 12661function integrateSecX(x) { 12662 const xHalf = .5 * x; 12663 const sin = Math.sin(xHalf); 12664 const cos = Math.cos(xHalf); 12665 return Math.log(sin + cos) - Math.log(cos - sin); 12666} 12667/** 12668* Interpolates globe center between two locations while preserving apparent rotation speed during interpolation. 12669* @param start - The starting location of the interpolation. 12670* @param deltaLng - Longitude delta to the end of the interpolation. 12671* @param deltaLat - Latitude delta to the end of the interpolation. 12672* @param t - The interpolation point in [0..1], where 0 is starting location, 1 is end location and other values are in between. 12673* @returns The interpolated location. 12674*/ 12675function interpolateLngLatForGlobe(start, deltaLng, deltaLat, t) { 12676 const interpolatedLat = start.lat + deltaLat * t; 12677 if (Math.abs(deltaLat) > 1) { 12678 const endLat = start.lat + deltaLat; 12679 const samplePointStart = (Math.sign(endLat) !== Math.sign(start.lat) ? -Math.abs(start.lat) : Math.abs(start.lat)) * Math.PI / 180; 12680 const samplePointEnd = Math.abs(start.lat + deltaLat) * Math.PI / 180; 12681 const valueT = integrateSecX(samplePointStart + t * (samplePointEnd - samplePointStart)); 12682 const valueStart = integrateSecX(samplePointStart); 12683 const valueEnd = integrateSecX(samplePointEnd); 12684 const newT = (valueT - valueStart) / (valueEnd - valueStart); 12685 const interpolatedLng = start.lng + deltaLng * newT; 12686 return new LngLat(interpolatedLng, interpolatedLat); 12687 } else { 12688 const interpolatedLng = start.lng + deltaLng * t; 12689 return new LngLat(interpolatedLng, interpolatedLat); 12690 } 12691} 12692/** 12693* Returns the two intersection points of the ray and a sphere centered at the origin, 12694* or null if no intersection occurs. 12695* The intersections are encoded as the parameter for parametric ray equation, 12696* with `tMin` being the first intersection and `tMax` being the second. 12697* The quadratic is solved as suggested in Ray Tracing Gems, chapter 7, since the 12698* schoolbook approach leads to floating point precision issues: 12699* https://www.realtimerendering.com/raytracinggems/rtg/index.html 12700* @param origin - The ray origin. 12701* @param direction - The normalized ray direction. 12702* @param radius - The sphere radius, defaults to the unit sphere of the planet. 12703*/ 12704function raySphereIntersection(origin, direction, radius = 1) { 12705 const originDotDirection = dot(origin, direction); 12706 const radiusSquared = radius * radius; 12707 const inner = createVec3f64(); 12708 const scaledDir = createVec3f64(); 12709 scale$1(scaledDir, direction, originDotDirection); 12710 sub(inner, origin, scaledDir); 12711 const discriminant = radiusSquared - dot(inner, inner); 12712 if (discriminant < 0) return null; 12713 const c = dot(origin, origin) - radiusSquared; 12714 const q = -originDotDirection + (originDotDirection < 0 ? 1 : -1) * Math.sqrt(discriminant); 12715 const t0 = c / q; 12716 const t1 = q; 12717 return { 12718 tMin: Math.min(t0, t1), 12719 tMax: Math.max(t0, t1) 12720 }; 12721} 12722/** Height of the atmosphere relative to the globe radius, 100 km for Earth. */ 12723const ATMOSPHERE_HEIGHT_TO_GLOBE_RADIUS = 1e5 / 6371e3; 12724/** 12725* Returns how much of the atmosphere to draw for a camera at the given altitude above the globe. 12726* It is 0 inside the atmosphere and reaches 1 at ten times the atmosphere height. The sky shader 12727* fades out along the same curve. 12728*/ 12729function getAtmosphereAltitudeBlend(altitude, globeRadius) { 12730 const atmosphereHeight = globeRadius * ATMOSPHERE_HEIGHT_TO_GLOBE_RADIUS; 12731 const x = clamp((altitude - atmosphereHeight) / (9 * atmosphereHeight), 0, 1); 12732 return x * x * (3 - 2 * x); 12733} 12734//#endregion 12735//#region src/util/primitives/bounding_volume_cache.ts 12736var BoundingVolumeCache = class { 12737 constructor(boundingVolumeFactory) { 12738 this._cachePrevious = /* @__PURE__ */ new Map(); 12739 this._cache = /* @__PURE__ */ new Map(); 12740 this._hadAnyChanges = false; 12741 this._boundingVolumeFactory = boundingVolumeFactory; 12742 } 12743 /** 12744 * Prepares bounding volume cache for next frame. Call at the beginning of a frame. 12745 * Bounding volume of any tile accesses in the last frame is kept in the cache, other (unaccessed) bounding volumes are deleted. 12746 */ 12747 swapBuffers() { 12748 if (!this._hadAnyChanges) return; 12749 const oldCache = this._cachePrevious; 12750 this._cachePrevious = this._cache; 12751 this._cache = oldCache; 12752 this._cache.clear(); 12753 this._hadAnyChanges = false; 12754 } 12755 /** 12756 * Returns the bounding volume of the specified tile, fetching it from cache or creating it using the factory function if needed. 12757 * @param tileID - Tile x, y and z for zoom. 12758 */ 12759 getTileBoundingVolume(tileID, wrap, elevation, options) { 12760 const key = `${tileID.z}_${tileID.x}_${tileID.y}_${options?.terrain ? "t" : ""}_${Math.round(elevation)}`; 12761 const cached = this._cache.get(key); 12762 if (cached) return cached; 12763 const cachedPrevious = this._cachePrevious.get(key); 12764 if (cachedPrevious) { 12765 this._cache.set(key, cachedPrevious); 12766 return cachedPrevious; 12767 } 12768 const boundingVolume = this._boundingVolumeFactory(tileID, wrap, elevation, options); 12769 this._cache.set(key, boundingVolume); 12770 this._hadAnyChanges = true; 12771 return boundingVolume; 12772 } 12773}; 12774//#endregion 12775//#region src/util/primitives/convex_volume.ts 12776/** 12777* A general convex bounding volume, defined by a set of points. 12778*/ 12779var ConvexVolume = class ConvexVolume { 12780 /** 12781 * Creates an instance of a general convex bounding volume. 12782 * Note that the provided points array is used *as is*, its contents are not copied! 12783 * 12784 * Additionally, an AABB must be provided for rejecting frustum intersections. 12785 * This AABB does not need to bound this convex volume (it may be smaller), 12786 * but it *must* accurately bound the actual shape this volume is approximating. 12787 * @param points - Points forming the convex shape. Note that this array reference is used *as is*, its contents are not copied! 12788 * @param min - The bounding AABB's min point. 12789 * @param max - The bounding AABB's min point. 12790 */ 12791 constructor(points, planes, min, max) { 12792 this.min = min; 12793 this.max = max; 12794 this.points = points; 12795 this.planes = planes; 12796 } 12797 /** 12798 * Creates a convex BV equivalent to the specified AABB. 12799 * @param min - The AABB's min point. 12800 * @param max - The AABB's max point. 12801 */ 12802 static fromAabb(min, max) { 12803 const points = []; 12804 for (let i = 0; i < 8; i++) points.push([ 12805 (i >> 0 & 1) === 1 ? max[0] : min[0], 12806 (i >> 1 & 1) === 1 ? max[1] : min[1], 12807 (i >> 2 & 1) === 1 ? max[2] : min[2] 12808 ]); 12809 return new ConvexVolume(points, [ 12810 [ 12811 -1, 12812 0, 12813 0, 12814 max[0] 12815 ], 12816 [ 12817 1, 12818 0, 12819 0, 12820 -min[0] 12821 ], 12822 [ 12823 0, 12824 -1, 12825 0, 12826 max[1] 12827 ], 12828 [ 12829 0, 12830 1, 12831 0, 12832 -min[1] 12833 ], 12834 [ 12835 0, 12836 0, 12837 -1, 12838 max[2] 12839 ], 12840 [ 12841 0, 12842 0, 12843 1, 12844 -min[2] 12845 ] 12846 ], min, max); 12847 } 12848 /** 12849 * Creates a convex bounding volume that is actually an oriented bounding box created from the specified center, half-size and rotation angles. 12850 * @param center - Center of the OBB. 12851 * @param halfSize - The half-size of the OBB in each axis. The box will extend by this value in each direction for the given axis. 12852 * @param angles - The rotation of the box. Euler angles, in degrees. 12853 */ 12854 static fromCenterSizeAngles(center, halfSize, angles) { 12855 const q = fromEuler([], angles[0], angles[1], angles[2]); 12856 const axisX = transformQuat([], [ 12857 halfSize[0], 12858 0, 12859 0 12860 ], q); 12861 const axisY = transformQuat([], [ 12862 0, 12863 halfSize[1], 12864 0 12865 ], q); 12866 const axisZ = transformQuat([], [ 12867 0, 12868 0, 12869 halfSize[2] 12870 ], q); 12871 const min = [...center]; 12872 const max = [...center]; 12873 for (let i = 0; i < 8; i++) for (let axis = 0; axis < 3; axis++) { 12874 const point = center[axis] + axisX[axis] * ((i >> 0 & 1) === 1 ? 1 : -1) + axisY[axis] * ((i >> 1 & 1) === 1 ? 1 : -1) + axisZ[axis] * ((i >> 2 & 1) === 1 ? 1 : -1); 12875 min[axis] = Math.min(min[axis], point); 12876 max[axis] = Math.max(max[axis], point); 12877 } 12878 const points = []; 12879 for (let i = 0; i < 8; i++) { 12880 const p = [...center]; 12881 add(p, p, scale$1([], axisX, (i >> 0 & 1) === 1 ? 1 : -1)); 12882 add(p, p, scale$1([], axisY, (i >> 1 & 1) === 1 ? 1 : -1)); 12883 add(p, p, scale$1([], axisZ, (i >> 2 & 1) === 1 ? 1 : -1)); 12884 points.push(p); 12885 } 12886 return new ConvexVolume(points, [ 12887 [...axisX, -dot(axisX, points[0])], 12888 [...axisY, -dot(axisY, points[0])], 12889 [...axisZ, -dot(axisZ, points[0])], 12890 [ 12891 -axisX[0], 12892 -axisX[1], 12893 -axisX[2], 12894 -dot(axisX, points[7]) 12895 ], 12896 [ 12897 -axisY[0], 12898 -axisY[1], 12899 -axisY[2], 12900 -dot(axisY, points[7]) 12901 ], 12902 [ 12903 -axisZ[0], 12904 -axisZ[1], 12905 -axisZ[2], 12906 -dot(axisZ, points[7]) 12907 ] 12908 ], min, max); 12909 } 12910 /** 12911 * Performs an approximate frustum-obb intersection test. 12912 */ 12913 intersectsFrustum(frustum) { 12914 let fullyInside = true; 12915 const boxPointCount = this.points.length; 12916 const boxPlaneCount = this.planes.length; 12917 const frustumPlaneCount = frustum.planes.length; 12918 const frustumPointCount = frustum.points.length; 12919 for (let i = 0; i < frustumPlaneCount; i++) { 12920 const plane = frustum.planes[i]; 12921 let boxPointsPassed = 0; 12922 for (let j = 0; j < boxPointCount; j++) { 12923 const point = this.points[j]; 12924 if (plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] + plane[3] >= 0) boxPointsPassed++; 12925 } 12926 if (boxPointsPassed === 0) return 0; 12927 if (boxPointsPassed < boxPointCount) fullyInside = false; 12928 } 12929 if (fullyInside) return 2; 12930 for (let i = 0; i < boxPlaneCount; i++) { 12931 const plane = this.planes[i]; 12932 let frustumPointsPassed = 0; 12933 for (let j = 0; j < frustumPointCount; j++) { 12934 const point = frustum.points[j]; 12935 if (plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] + plane[3] >= 0) frustumPointsPassed++; 12936 } 12937 if (frustumPointsPassed === 0) return 0; 12938 } 12939 return 1; 12940 } 12941 /** 12942 * Performs an intersection test with a halfspace. 12943 */ 12944 intersectsPlane(plane) { 12945 const pointCount = this.points.length; 12946 let positivePoints = 0; 12947 for (let i = 0; i < pointCount; i++) { 12948 const point = this.points[i]; 12949 if (plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] + plane[3] >= 0) positivePoints++; 12950 } 12951 if (positivePoints === pointCount) return 2; 12952 if (positivePoints === 0) return 0; 12953 return 1; 12954 } 12955}; 12956//#endregion 12957//#region src/geo/projection/globe_covering_tiles_details_provider.ts 12958/** 12959* Computes distance of a point to a tile in an arbitrary axis. 12960* World is assumed to have size 1, distance returned is to the nearer tile edge. 12961* @param point - Point position. 12962* @param tile - Tile position. 12963* @param tileSize - Tile size. 12964*/ 12965function distanceToTileSimple(point, tile, tileSize) { 12966 const delta = point - tile; 12967 return delta < 0 ? -delta : Math.max(0, delta - tileSize); 12968} 12969function distanceToTileWrapX(pointX, pointY, tileCornerX, tileCornerY, tileSize) { 12970 const tileCornerToPointX = pointX - tileCornerX; 12971 let distanceX; 12972 if (tileCornerToPointX < 0) distanceX = Math.min(-tileCornerToPointX, 1 + tileCornerToPointX - tileSize); 12973 else if (tileCornerToPointX > tileSize) distanceX = Math.min(Math.max(tileCornerToPointX - tileSize, 0), 1 - tileCornerToPointX); 12974 else distanceX = 0; 12975 return Math.max(distanceX, distanceToTileSimple(pointY, tileCornerY, tileSize)); 12976} 12977var GlobeCoveringTilesDetailsProvider = class { 12978 constructor() { 12979 this._boundingVolumeCache = new BoundingVolumeCache(this._computeTileBoundingVolume); 12980 } 12981 /** 12982 * Prepares the internal bounding volume cache for the next frame. 12983 */ 12984 prepareNextFrame() { 12985 this._boundingVolumeCache.swapBuffers(); 12986 } 12987 /** 12988 * Returns the distance of a point to a square tile. If the point is inside the tile, returns 0. 12989 * Assumes the world to be of size 1. 12990 * Handles distances on a sphere correctly: X is wrapped when crossing the antimeridian, 12991 * when crossing the poles Y is mirrored and X is shifted by half world size. 12992 */ 12993 distanceToTile2d(pointX, pointY, tileID, _bv) { 12994 const scale = 1 << tileID.z; 12995 const tileMercatorSize = 1 / scale; 12996 const tileCornerX = tileID.x / scale; 12997 const tileCornerY = tileID.y / scale; 12998 const halfWorld = .5; 12999 let smallestDistance = 2; 13000 smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX, tileCornerY, tileMercatorSize)); 13001 smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, -tileCornerY - tileMercatorSize, tileMercatorSize)); 13002 smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, 2 - tileCornerY - tileMercatorSize, tileMercatorSize)); 13003 return smallestDistance; 13004 } 13005 /** 13006 * Returns the wrap value for a given tile, computed so that tiles will remain loaded when crossing the antimeridian. 13007 */ 13008 getWrap(centerCoord, tileID, _parentWrap) { 13009 const scale = 1 << tileID.z; 13010 const tileMercatorSize = 1 / scale; 13011 const tileX = tileID.x / scale; 13012 const distanceCurrent = distanceToTileSimple(centerCoord.x, tileX, tileMercatorSize); 13013 const distanceLeft = distanceToTileSimple(centerCoord.x, tileX - 1, tileMercatorSize); 13014 const distanceRight = distanceToTileSimple(centerCoord.x, tileX + 1, tileMercatorSize); 13015 const distanceSmallest = Math.min(distanceCurrent, distanceLeft, distanceRight); 13016 if (distanceSmallest === distanceRight) return 1; 13017 if (distanceSmallest === distanceLeft) return -1; 13018 return 0; 13019 } 13020 allowVariableZoom(transform, options) { 13021 return coveringZoomLevel(transform, options) > 4; 13022 } 13023 allowWorldCopies() { 13024 return false; 13025 } 13026 getTileBoundingVolume(tileID, wrap, elevation, options) { 13027 return this._boundingVolumeCache.getTileBoundingVolume(tileID, wrap, elevation, options); 13028 } 13029 /** 13030 * Computes the bounding volume of a tile for culling. The volume reaches up to `elevation`, 13031 * which carries the content elevation allowance (e.g. elevated symbols); terrain elevations 13032 * widen the volume but never shrink that allowance. 13033 */ 13034 _computeTileBoundingVolume(tileID, wrap, elevation, options) { 13035 let minElevation = Math.min(0, elevation); 13036 let maxElevation = Math.max(0, elevation); 13037 if (options?.terrain) { 13038 const overscaledTileID = new OverscaledTileID(tileID.z, wrap, tileID.z, tileID.x, tileID.y); 13039 const minMax = options.terrain.getMinMaxElevation(overscaledTileID); 13040 minElevation = minMax.minElevation ?? minElevation; 13041 maxElevation = Math.max(minMax.maxElevation ?? maxElevation, maxElevation); 13042 } 13043 minElevation /= earthRadius; 13044 maxElevation /= earthRadius; 13045 minElevation += 1; 13046 maxElevation += 1; 13047 if (tileID.z <= 0) return ConvexVolume.fromAabb([ 13048 -maxElevation, 13049 -maxElevation, 13050 -maxElevation 13051 ], [ 13052 maxElevation, 13053 maxElevation, 13054 maxElevation 13055 ]); 13056 else if (tileID.z === 1) return ConvexVolume.fromAabb([ 13057 tileID.x === 0 ? -maxElevation : 0, 13058 tileID.y === 0 ? 0 : -maxElevation, 13059 -maxElevation 13060 ], [ 13061 tileID.x === 0 ? 0 : maxElevation, 13062 tileID.y === 0 ? maxElevation : 0, 13063 maxElevation 13064 ]); 13065 else { 13066 const corners = [ 13067 projectTileCoordinatesToSphere(0, 0, tileID.x, tileID.y, tileID.z), 13068 projectTileCoordinatesToSphere(EXTENT, 0, tileID.x, tileID.y, tileID.z), 13069 projectTileCoordinatesToSphere(EXTENT, EXTENT, tileID.x, tileID.y, tileID.z), 13070 projectTileCoordinatesToSphere(0, EXTENT, tileID.x, tileID.y, tileID.z) 13071 ]; 13072 const extremesPoints = []; 13073 for (const c of corners) extremesPoints.push(scale$1([], c, maxElevation)); 13074 if (maxElevation !== minElevation) for (const c of corners) extremesPoints.push(scale$1([], c, minElevation)); 13075 if (tileID.y === 0) extremesPoints.push([ 13076 0, 13077 1, 13078 0 13079 ]); 13080 if (tileID.y === (1 << tileID.z) - 1) extremesPoints.push([ 13081 0, 13082 -1, 13083 0 13084 ]); 13085 const aabbMin = [ 13086 1, 13087 1, 13088 1 13089 ]; 13090 const aabbMax = [ 13091 -1, 13092 -1, 13093 -1 13094 ]; 13095 for (const c of extremesPoints) for (let i = 0; i < 3; i++) { 13096 aabbMin[i] = Math.min(aabbMin[i], c[i]); 13097 aabbMax[i] = Math.max(aabbMax[i], c[i]); 13098 } 13099 const center = projectTileCoordinatesToSphere(EXTENT / 2, EXTENT / 2, tileID.x, tileID.y, tileID.z); 13100 const centerEast = cross([], [ 13101 0, 13102 1, 13103 0 13104 ], center); 13105 normalize(centerEast, centerEast); 13106 const north = cross([], center, centerEast); 13107 normalize(north, north); 13108 const axisEast = cross([], corners[2], corners[1]); 13109 normalize(axisEast, axisEast); 13110 const axisWest = cross([], corners[0], corners[3]); 13111 normalize(axisWest, axisWest); 13112 extremesPoints.push(scale$1([], center, maxElevation)); 13113 if (tileID.y >= (1 << tileID.z) / 2) extremesPoints.push(scale$1([], projectTileCoordinatesToSphere(EXTENT / 2, 0, tileID.x, tileID.y, tileID.z), maxElevation)); 13114 if (tileID.y < (1 << tileID.z) / 2) extremesPoints.push(scale$1([], projectTileCoordinatesToSphere(EXTENT / 2, EXTENT, tileID.x, tileID.y, tileID.z), maxElevation)); 13115 const upDownMinMax = findAxisMinMax(center, extremesPoints); 13116 const northSouthMinMax = findAxisMinMax(north, extremesPoints); 13117 const planeUp = [ 13118 -center[0], 13119 -center[1], 13120 -center[2], 13121 upDownMinMax.max 13122 ]; 13123 const planeDown = [ 13124 center[0], 13125 center[1], 13126 center[2], 13127 -upDownMinMax.min 13128 ]; 13129 const planeNorth = [ 13130 -north[0], 13131 -north[1], 13132 -north[2], 13133 northSouthMinMax.max 13134 ]; 13135 const planeSouth = [ 13136 north[0], 13137 north[1], 13138 north[2], 13139 -northSouthMinMax.min 13140 ]; 13141 const planeEast = [...axisEast, 0]; 13142 const planeWest = [...axisWest, 0]; 13143 const points = []; 13144 if (tileID.y === 0) points.push(threePlaneIntersection(planeWest, planeEast, planeUp), threePlaneIntersection(planeWest, planeEast, planeDown)); 13145 else points.push(threePlaneIntersection(planeNorth, planeEast, planeUp), threePlaneIntersection(planeNorth, planeEast, planeDown), threePlaneIntersection(planeNorth, planeWest, planeUp), threePlaneIntersection(planeNorth, planeWest, planeDown)); 13146 if (tileID.y === (1 << tileID.z) - 1) points.push(threePlaneIntersection(planeWest, planeEast, planeUp), threePlaneIntersection(planeWest, planeEast, planeDown)); 13147 else points.push(threePlaneIntersection(planeSouth, planeEast, planeUp), threePlaneIntersection(planeSouth, planeEast, planeDown), threePlaneIntersection(planeSouth, planeWest, planeUp), threePlaneIntersection(planeSouth, planeWest, planeDown)); 13148 return new ConvexVolume(points, [ 13149 planeUp, 13150 planeDown, 13151 planeNorth, 13152 planeSouth, 13153 planeEast, 13154 planeWest 13155 ], aabbMin, aabbMax); 13156 } 13157 } 13158}; 13159function findAxisMinMax(axis, points) { 13160 let min = Infinity; 13161 let max = -Infinity; 13162 for (const c of points) { 13163 const dot$1 = dot(axis, c); 13164 min = Math.min(min, dot$1); 13165 max = Math.max(max, dot$1); 13166 } 13167 return { 13168 min, 13169 max 13170 }; 13171} 13172//#endregion 13173//#region src/geo/projection/vertical_perspective_transform.ts 13174const GLOBE_SAMPLES = 256; 13175const GLOBE_BISECT_EPSILON_T = 1e-12; 13176/** Latitudes outside the mercator range project past the world edge; the globe mesh still covers them. */ 13177const MAX_MERCATOR_Y = 1 - 1e-9; 13178/** Two points on the unit globe closer than this, a micrometre, are the same point. */ 13179const SAME_POINT_DISTANCE = 1e-12; 13180/** A camera whose horizontal offset is this small relative to its distance is straight above the center. */ 13181const STRAIGHT_ABOVE_RATIO = 1e-9; 13182var VerticalPerspectiveTransform = class VerticalPerspectiveTransform { 13183 get pixelsToClipSpaceMatrix() { 13184 return this._helper.pixelsToClipSpaceMatrix; 13185 } 13186 get clipSpaceToPixelsMatrix() { 13187 return this._helper.clipSpaceToPixelsMatrix; 13188 } 13189 get pixelsToGLUnits() { 13190 return this._helper.pixelsToGLUnits; 13191 } 13192 get centerOffset() { 13193 return this._helper.centerOffset; 13194 } 13195 get size() { 13196 return this._helper.size; 13197 } 13198 get rotationMatrix() { 13199 return this._helper.rotationMatrix; 13200 } 13201 get centerPoint() { 13202 return this._helper.centerPoint; 13203 } 13204 get pixelsPerMeter() { 13205 return this._helper.pixelsPerMeter; 13206 } 13207 setMinZoom(zoom) { 13208 this._helper.setMinZoom(zoom); 13209 } 13210 setMaxZoom(zoom) { 13211 this._helper.setMaxZoom(zoom); 13212 } 13213 setMinPitch(pitch) { 13214 this._helper.setMinPitch(pitch); 13215 } 13216 setMaxPitch(pitch) { 13217 this._helper.setMaxPitch(pitch); 13218 } 13219 setRenderWorldCopies(renderWorldCopies) { 13220 this._helper.setRenderWorldCopies(renderWorldCopies); 13221 } 13222 setBearing(bearing) { 13223 this._helper.setBearing(bearing); 13224 } 13225 setPitch(pitch) { 13226 this._helper.setPitch(pitch); 13227 } 13228 setRoll(roll) { 13229 this._helper.setRoll(roll); 13230 } 13231 setFov(fov) { 13232 this._helper.setFov(fov); 13233 } 13234 setZoom(zoom) { 13235 this._helper.setZoom(zoom); 13236 } 13237 setCenter(center) { 13238 this._helper.setCenter(center); 13239 } 13240 setElevation(elevation) { 13241 this._helper.setElevation(elevation); 13242 } 13243 setMinElevationForCurrentTile(elevation) { 13244 this._helper.setMinElevationForCurrentTile(elevation); 13245 } 13246 setPadding(padding) { 13247 this._helper.setPadding(padding); 13248 } 13249 interpolatePadding(start, target, t) { 13250 this._helper.interpolatePadding(start, target, t); 13251 } 13252 isPaddingEqual(padding) { 13253 return this._helper.isPaddingEqual(padding); 13254 } 13255 resize(width, height) { 13256 this._helper.resize(width, height); 13257 } 13258 getMaxBounds() { 13259 return this._helper.getMaxBounds(); 13260 } 13261 setMaxBounds(bounds) { 13262 this._helper.setMaxBounds(bounds); 13263 } 13264 setConstrainOverride(constrain) { 13265 this._helper.setConstrainOverride(constrain); 13266 } 13267 overrideNearFarZ(nearZ, farZ) { 13268 this._helper.overrideNearFarZ(nearZ, farZ); 13269 } 13270 clearNearFarZOverride() { 13271 this._helper.clearNearFarZOverride(); 13272 } 13273 getCameraQueryGeometry(queryGeometry) { 13274 return this._helper.getCameraQueryGeometry(this.getCameraPoint(), queryGeometry); 13275 } 13276 get tileSize() { 13277 return this._helper.tileSize; 13278 } 13279 get tileZoom() { 13280 return this._helper.tileZoom; 13281 } 13282 get scale() { 13283 return this._helper.scale; 13284 } 13285 get worldSize() { 13286 return this._helper.worldSize; 13287 } 13288 get width() { 13289 return this._helper.width; 13290 } 13291 get height() { 13292 return this._helper.height; 13293 } 13294 get lngRange() { 13295 return this._helper.lngRange; 13296 } 13297 get latRange() { 13298 return this._helper.latRange; 13299 } 13300 get minZoom() { 13301 return this._helper.minZoom; 13302 } 13303 get maxZoom() { 13304 return this._helper.maxZoom; 13305 } 13306 get zoom() { 13307 return this._helper.zoom; 13308 } 13309 get center() { 13310 return this._helper.center; 13311 } 13312 get minPitch() { 13313 return this._helper.minPitch; 13314 } 13315 get maxPitch() { 13316 return this._helper.maxPitch; 13317 } 13318 get pitch() { 13319 return this._helper.pitch; 13320 } 13321 get pitchInRadians() { 13322 return this._helper.pitchInRadians; 13323 } 13324 get roll() { 13325 return this._helper.roll; 13326 } 13327 get rollInRadians() { 13328 return this._helper.rollInRadians; 13329 } 13330 get bearing() { 13331 return this._helper.bearing; 13332 } 13333 get bearingInRadians() { 13334 return this._helper.bearingInRadians; 13335 } 13336 get fov() { 13337 return this._helper.fov; 13338 } 13339 get fovInRadians() { 13340 return this._helper.fovInRadians; 13341 } 13342 get elevation() { 13343 return this._helper.elevation; 13344 } 13345 get minElevationForCurrentTile() { 13346 return this._helper.minElevationForCurrentTile; 13347 } 13348 get padding() { 13349 return this._helper.padding; 13350 } 13351 get unmodified() { 13352 return this._helper.unmodified; 13353 } 13354 get renderWorldCopies() { 13355 return this._helper.renderWorldCopies; 13356 } 13357 get constrainOverride() { 13358 return this._helper.constrainOverride; 13359 } 13360 get nearZ() { 13361 return this._helper.nearZ; 13362 } 13363 get farZ() { 13364 return this._helper.farZ; 13365 } 13366 get autoCalculateNearFarZ() { 13367 return this._helper.autoCalculateNearFarZ; 13368 } 13369 setTransitionState(_value) {} 13370 /** 13371 * @param options - Initial state. Ignored when `sharedHelper` is given, which already carries it. 13372 * @param sharedHelper - Camera to use instead of owning one, so that a composing transform such as 13373 * {@link GlobeTransform} keeps a single copy of the state rather than one per child. Its owner then drives 13374 * {@link _calcMatrices}, because a helper has only one `calcMatrices` callback. 13375 */ 13376 constructor(options, sharedHelper) { 13377 this._cachedClippingPlane = createVec4f64(); 13378 this._projectionMatrix = createIdentityMat4f64(); 13379 this._globeViewProjMatrix32f = createIdentityMat4f32(); 13380 this._globeViewProjMatrixF64 = createIdentityMat4f64(); 13381 this._globeViewProjMatrixF64Inverted = createIdentityMat4f64(); 13382 this._globeProjMatrixInverted = createIdentityMat4f64(); 13383 this._cameraPosition = createVec3f64(); 13384 this.defaultConstrain = (lngLat, zoom) => { 13385 const constrainedLat = clamp(lngLat.lat, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE); 13386 const constrainedZoom = clamp(+zoom, this.minZoom + getZoomAdjustment(0, constrainedLat), this.maxZoom); 13387 return { 13388 center: new LngLat(lngLat.lng, constrainedLat), 13389 zoom: constrainedZoom 13390 }; 13391 }; 13392 this.applyConstrain = (lngLat, zoom) => { 13393 return this._helper.applyConstrain(lngLat, zoom); 13394 }; 13395 this._helper = sharedHelper ?? new TransformHelper({ 13396 calcMatrices: () => this._calcMatrices(), 13397 defaultConstrain: (center, zoom) => { 13398 return this.defaultConstrain(center, zoom); 13399 } 13400 }, options); 13401 this._coveringTilesDetailsProvider = new GlobeCoveringTilesDetailsProvider(); 13402 } 13403 clone() { 13404 const clone = new VerticalPerspectiveTransform(); 13405 clone.apply(this, false); 13406 return clone; 13407 } 13408 apply(that, constrain) { 13409 this._helper.apply(that, constrain); 13410 } 13411 get projectionMatrix() { 13412 return this._projectionMatrix; 13413 } 13414 get modelViewProjectionMatrix() { 13415 return this._globeViewProjMatrixF64; 13416 } 13417 get inverseProjectionMatrix() { 13418 return this._globeProjMatrixInverted; 13419 } 13420 get cameraPosition() { 13421 const copy = createVec3f64(); 13422 copy[0] = this._cameraPosition[0]; 13423 copy[1] = this._cameraPosition[1]; 13424 copy[2] = this._cameraPosition[2]; 13425 return copy; 13426 } 13427 get cameraToCenterDistance() { 13428 return this._helper.cameraToCenterDistance; 13429 } 13430 getProjectionData(params) { 13431 const { overscaledTileID, applyGlobeMatrix } = params; 13432 const mercatorTileCoordinates = this._helper.getMercatorTileCoordinates(overscaledTileID); 13433 return { 13434 mainMatrix: this._globeViewProjMatrix32f, 13435 tileMercatorCoords: mercatorTileCoordinates, 13436 clippingPlane: this._cachedClippingPlane, 13437 projectionTransition: applyGlobeMatrix ? 1 : 0, 13438 fallbackMatrix: this._globeViewProjMatrix32f, 13439 clipAntimeridian: overscaledTileID?.canonical.z === 0 13440 }; 13441 } 13442 _computeClippingPlane(globeRadiusPixels) { 13443 const pitch = this.pitchInRadians; 13444 const distanceCameraToB = this.cameraToCenterDistance / globeRadiusPixels; 13445 const radius = 1; 13446 const distanceCameraToA = Math.sin(pitch) * distanceCameraToB; 13447 const distanceAtoC = Math.cos(pitch) * distanceCameraToB + radius; 13448 const tangentPlaneDistanceToC = radius / Math.sqrt(distanceCameraToA * distanceCameraToA + distanceAtoC * distanceAtoC) * radius; 13449 let vectorCtoCamX = -distanceCameraToA; 13450 let vectorCtoCamY = distanceAtoC; 13451 const vectorCtoCamLength = Math.sqrt(vectorCtoCamX * vectorCtoCamX + vectorCtoCamY * vectorCtoCamY); 13452 vectorCtoCamX /= vectorCtoCamLength; 13453 vectorCtoCamY /= vectorCtoCamLength; 13454 const planeVector = [ 13455 0, 13456 vectorCtoCamX, 13457 vectorCtoCamY 13458 ]; 13459 rotateZ$1(planeVector, planeVector, [ 13460 0, 13461 0, 13462 0 13463 ], -this.bearingInRadians); 13464 rotateX$1(planeVector, planeVector, [ 13465 0, 13466 0, 13467 0 13468 ], -1 * this.center.lat * Math.PI / 180); 13469 rotateY(planeVector, planeVector, [ 13470 0, 13471 0, 13472 0 13473 ], this.center.lng * Math.PI / 180); 13474 const scale = 1 / length(planeVector); 13475 scale$1(planeVector, planeVector, scale); 13476 return [...planeVector, -tangentPlaneDistanceToC * scale]; 13477 } 13478 /** {@inheritDoc ITransform.isLocationOccluded} */ 13479 isLocationOccluded(lngLat, terrain, elevation) { 13480 const coverage = terrain?.getCoverageIndex(); 13481 elevation ??= coverage ? terrain.getElevationForLngLat(lngLat, this) : 0; 13482 const location = raisedSurfaceVector(lngLat, elevation); 13483 if (!this.isSurfacePointVisible(location)) return true; 13484 if (!coverage) return false; 13485 const p = this._projectSurfacePointToScreen(location); 13486 const origin = this.cameraPosition; 13487 const direction = this.getRayDirectionFromPixel(p); 13488 const tLocation = rayParameter(origin, direction, location); 13489 if (tLocation <= 0) return true; 13490 const hit = this.screenTerrainPointToMercatorCoordinate(p, terrain); 13491 return hit != null && rayParameter(origin, direction, raisedSurfaceVector(hit.toLngLat(), hit.z)) < tLocation * .99; 13492 } 13493 transformLightDirection(dir) { 13494 const sphereX = this._helper._center.lng * Math.PI / 180; 13495 const sphereY = this._helper._center.lat * Math.PI / 180; 13496 const len = Math.cos(sphereY); 13497 const spherePos = [ 13498 Math.sin(sphereX) * len, 13499 Math.sin(sphereY), 13500 Math.cos(sphereX) * len 13501 ]; 13502 const axisRight = [ 13503 spherePos[2], 13504 0, 13505 -spherePos[0] 13506 ]; 13507 const axisDown = [ 13508 0, 13509 0, 13510 0 13511 ]; 13512 cross(axisDown, axisRight, spherePos); 13513 normalize(axisRight, axisRight); 13514 normalize(axisDown, axisDown); 13515 const transformed = [ 13516 axisRight[0] * dir[0] + axisDown[0] * dir[1] + spherePos[0] * dir[2], 13517 axisRight[1] * dir[0] + axisDown[1] * dir[1] + spherePos[1] * dir[2], 13518 axisRight[2] * dir[0] + axisDown[2] * dir[1] + spherePos[2] * dir[2] 13519 ]; 13520 const normalized = [ 13521 0, 13522 0, 13523 0 13524 ]; 13525 normalize(normalized, transformed); 13526 return normalized; 13527 } 13528 getPixelScale() { 13529 return 1 / Math.cos(this._helper._center.lat * Math.PI / 180); 13530 } 13531 getCircleRadiusCorrection() { 13532 return Math.cos(this._helper._center.lat * Math.PI / 180); 13533 } 13534 getPitchedTextCorrection(textAnchorX, textAnchorY, tileID) { 13535 const mercator = tileCoordinatesToMercatorCoordinates(textAnchorX, textAnchorY, tileID.canonical); 13536 const angular = mercatorCoordinatesToAngularCoordinatesRadians(mercator.x, mercator.y); 13537 return this.getCircleRadiusCorrection() / Math.cos(angular[1]); 13538 } 13539 projectTileCoordinates(x, y, unwrappedTileID, elevation) { 13540 const canonical = unwrappedTileID.canonical; 13541 const spherePos = projectTileCoordinatesToSphere(x, y, canonical.x, canonical.y, canonical.z); 13542 const vectorMultiplier = 1 + (elevation ?? 0) / earthRadius; 13543 const elevatedX = spherePos[0] * vectorMultiplier; 13544 const elevatedY = spherePos[1] * vectorMultiplier; 13545 const elevatedZ = spherePos[2] * vectorMultiplier; 13546 const pos = [ 13547 elevatedX, 13548 elevatedY, 13549 elevatedZ, 13550 1 13551 ]; 13552 transformMat4(pos, pos, this._globeViewProjMatrixF64); 13553 let isOccluded; 13554 if (vectorMultiplier <= 1) { 13555 const plane = this._cachedClippingPlane; 13556 isOccluded = plane[0] * spherePos[0] + plane[1] * spherePos[1] + plane[2] * spherePos[2] + plane[3] < 0; 13557 } else isOccluded = this._isLineOfSightBlocked(elevatedX, elevatedY, elevatedZ); 13558 return { 13559 point: new Point(pos[0] / pos[3], pos[1] / pos[3]), 13560 signedDistanceFromCamera: pos[3], 13561 isOccluded 13562 }; 13563 } 13564 /** 13565 * True when the segment from the camera to the given point, both in unit-globe coordinates, passes through the planet. 13566 */ 13567 _isLineOfSightBlocked(x, y, z) { 13568 const cam = this._cameraPosition; 13569 const dx = x - cam[0]; 13570 const dy = y - cam[1]; 13571 const dz = z - cam[2]; 13572 const lengthSq = dx * dx + dy * dy + dz * dz; 13573 if (lengthSq === 0) return false; 13574 const t = clamp(-(cam[0] * dx + cam[1] * dy + cam[2] * dz) / lengthSq, 0, 1); 13575 const cx = cam[0] + t * dx; 13576 const cy = cam[1] + t * dy; 13577 const cz = cam[2] + t * dz; 13578 return cx * cx + cy * cy + cz * cz < 1; 13579 } 13580 /** 13581 * @param calculateNearFarZ - Whether to compute the near/far Z range, or leave the range the helper already 13582 * holds. Defaults to {@link autoCalculateNearFarZ}; a composing transform such as {@link GlobeTransform} 13583 * overrides it so that its two children share a single depth range. 13584 */ 13585 _calcMatrices(calculateNearFarZ = this._helper.autoCalculateNearFarZ) { 13586 const globeRadiusPixels = getGlobeRadiusPixels(this.worldSize, this.center.lat); 13587 const globeMatrix = createMat4f64(); 13588 if (calculateNearFarZ) { 13589 this._helper._nearZ = .5; 13590 this._helper._farZ = this.cameraToCenterDistance + globeRadiusPixels * 2; 13591 } 13592 perspective(globeMatrix, this.fovInRadians, this.width / this.height, this._helper._nearZ, this._helper._farZ); 13593 const offset = this.centerOffset; 13594 globeMatrix[8] = -offset.x * 2 / this._helper._width; 13595 globeMatrix[9] = offset.y * 2 / this._helper._height; 13596 this._projectionMatrix = clone$1(globeMatrix); 13597 this._globeProjMatrixInverted = createMat4f64(); 13598 invert$1(this._globeProjMatrixInverted, globeMatrix); 13599 translate(globeMatrix, globeMatrix, [ 13600 0, 13601 0, 13602 -this.cameraToCenterDistance 13603 ]); 13604 rotateZ(globeMatrix, globeMatrix, this.rollInRadians); 13605 rotateX(globeMatrix, globeMatrix, -this.pitchInRadians); 13606 rotateZ(globeMatrix, globeMatrix, this.bearingInRadians); 13607 translate(globeMatrix, globeMatrix, [ 13608 0, 13609 0, 13610 -globeRadiusPixels 13611 ]); 13612 const scaleVec = createVec3f64(); 13613 scaleVec[0] = globeRadiusPixels; 13614 scaleVec[1] = globeRadiusPixels; 13615 scaleVec[2] = globeRadiusPixels; 13616 rotateX(globeMatrix, globeMatrix, this.center.lat * Math.PI / 180); 13617 rotateY$1(globeMatrix, globeMatrix, -this.center.lng * Math.PI / 180); 13618 scale(globeMatrix, globeMatrix, scaleVec); 13619 this._globeViewProjMatrixF64 = globeMatrix; 13620 this._globeViewProjMatrix32f = new Float32Array(globeMatrix); 13621 this._globeViewProjMatrixF64Inverted = createMat4f64(); 13622 invert$1(this._globeViewProjMatrixF64Inverted, globeMatrix); 13623 const zero = createVec3f64(); 13624 this._cameraPosition = createVec3f64(); 13625 this._cameraPosition[2] = this.cameraToCenterDistance / globeRadiusPixels; 13626 rotateZ$1(this._cameraPosition, this._cameraPosition, zero, -this.rollInRadians); 13627 rotateX$1(this._cameraPosition, this._cameraPosition, zero, this.pitchInRadians); 13628 rotateZ$1(this._cameraPosition, this._cameraPosition, zero, -this.bearingInRadians); 13629 add(this._cameraPosition, this._cameraPosition, [ 13630 0, 13631 0, 13632 1 13633 ]); 13634 rotateX$1(this._cameraPosition, this._cameraPosition, zero, -this.center.lat * Math.PI / 180); 13635 rotateY(this._cameraPosition, this._cameraPosition, zero, this.center.lng * Math.PI / 180); 13636 this._cachedClippingPlane = this._computeClippingPlane(globeRadiusPixels); 13637 const matrix = clone$1(this._globeViewProjMatrixF64Inverted); 13638 scale(matrix, matrix, [ 13639 1, 13640 1, 13641 -1 13642 ]); 13643 this._cachedFrustum = Frustum.fromInvProjectionMatrix(matrix, 1, 0, this._cachedClippingPlane, true); 13644 } 13645 calculateFogMatrix(_unwrappedTileID) { 13646 warnOnce("calculateFogMatrix is not supported on globe projection."); 13647 const m = createMat4f64(); 13648 identity(m); 13649 return m; 13650 } 13651 getVisibleUnwrappedCoordinates(tileID) { 13652 return [new UnwrappedTileID(0, tileID)]; 13653 } 13654 getCameraFrustum() { 13655 return this._cachedFrustum; 13656 } 13657 getClippingPlane() { 13658 return this._cachedClippingPlane; 13659 } 13660 getCoveringTilesDetailsProvider() { 13661 return this._coveringTilesDetailsProvider; 13662 } 13663 recalculateZoomAndCenter(terrain) { 13664 if (terrain) { 13665 warnOnce("terrain is not fully supported on vertical perspective projection."); 13666 return; 13667 } 13668 this._helper.recalculateZoomAndCenter(0); 13669 } 13670 maxPitchScaleFactor() { 13671 return 1; 13672 } 13673 getCameraPoint() { 13674 return this._helper.getCameraPoint(); 13675 } 13676 /** 13677 * The altitude of the rendered camera above sea level. The sphere keeps the center point at sea level whatever its 13678 * elevation (`_calcMatrices` does not apply it), so unlike on mercator the center elevation does not lift the camera. 13679 * {@link calculateCameraOptionsFromTo} is the inverse. 13680 */ 13681 getCameraAltitude() { 13682 return (length(this._cameraPosition) - 1) * earthRadius; 13683 } 13684 getCameraLngLat() { 13685 const surface = createVec3f64(); 13686 normalize(surface, this._cameraPosition); 13687 return sphereSurfacePointToCoordinates(surface); 13688 } 13689 populateCache(_coords) {} 13690 getBounds() { 13691 const xMid = this.width * .5; 13692 const yMid = this.height * .5; 13693 const testPoints = [ 13694 new Point(0, 0), 13695 new Point(xMid, 0), 13696 new Point(this.width, 0), 13697 new Point(this.width, yMid), 13698 new Point(this.width, this.height), 13699 new Point(xMid, this.height), 13700 new Point(0, this.height), 13701 new Point(0, yMid) 13702 ]; 13703 const projectedPoints = []; 13704 for (const p of testPoints) projectedPoints.push(this.unprojectScreenPoint(p)); 13705 let mostEast = 0, mostWest = 0, mostNorth = 0, mostSouth = 0; 13706 const center = this.center; 13707 for (const p of projectedPoints) { 13708 const dLng = differenceOfAnglesDegrees(center.lng, p.lng); 13709 const dLat = differenceOfAnglesDegrees(center.lat, p.lat); 13710 if (dLng < mostWest) mostWest = dLng; 13711 if (dLng > mostEast) mostEast = dLng; 13712 if (dLat < mostSouth) mostSouth = dLat; 13713 if (dLat > mostNorth) mostNorth = dLat; 13714 } 13715 const boundsArray = [ 13716 center.lng + mostWest, 13717 center.lat + mostSouth, 13718 center.lng + mostEast, 13719 center.lat + mostNorth 13720 ]; 13721 if (this.isSurfacePointOnScreen([ 13722 0, 13723 1, 13724 0 13725 ])) { 13726 boundsArray[3] = 90; 13727 boundsArray[0] = -180; 13728 boundsArray[2] = 180; 13729 } 13730 if (this.isSurfacePointOnScreen([ 13731 0, 13732 -1, 13733 0 13734 ])) { 13735 boundsArray[1] = -90; 13736 boundsArray[0] = -180; 13737 boundsArray[2] = 180; 13738 } 13739 return new LngLatBounds(boundsArray); 13740 } 13741 calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch) { 13742 return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch); 13743 } 13744 /** 13745 * Inverts the camera placement of `_calcMatrices` in unit-globe coordinates: the camera sits at radius 13746 * `1 + altitudeFrom / earthRadius` and looks at the center on the sea-level sphere, the target altitude only becoming 13747 * the center elevation (the inverse of {@link getCameraAltitude}). Pitch and bearing are read in the center's local 13748 * frame, +z up, +y north, +x east. A camera straight above the center keeps the transform's bearing. 13749 */ 13750 calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo) { 13751 const center = LngLat.convert(to); 13752 const camera = angularCoordinatesToSurfaceVector(LngLat.convert(from)); 13753 scale$1(camera, camera, 1 + altitudeFrom / earthRadius); 13754 const target = angularCoordinatesToSurfaceVector(center); 13755 const targetAtAltitude = scale$1(createVec3f64(), target, 1 + altitudeTo / earthRadius); 13756 const toCamera = subtract(createVec3f64(), camera, target); 13757 const distance$1 = length(toCamera); 13758 if (distance$1 < SAME_POINT_DISTANCE || distance(camera, targetAtAltitude) < SAME_POINT_DISTANCE) throw new Error("Can't calculate camera options with same From and To"); 13759 const zero = createVec3f64(); 13760 rotateY(toCamera, toCamera, zero, -degreesToRadians(center.lng)); 13761 rotateX$1(toCamera, toCamera, zero, degreesToRadians(center.lat)); 13762 const pitch = radiansToDegrees(Math.acos(clamp(toCamera[2] / distance$1, -1, 1))); 13763 const bearing = Math.hypot(toCamera[0], toCamera[1]) < distance$1 * STRAIGHT_ABOVE_RATIO ? this.bearing : radiansToDegrees(Math.atan2(-toCamera[0], -toCamera[1])); 13764 return { 13765 center, 13766 elevation: altitudeTo, 13767 zoom: scaleZoom(this.cameraToCenterDistance / distance$1 / getGlobeRadiusPixels(this.tileSize, center.lat)), 13768 pitch, 13769 bearing 13770 }; 13771 } 13772 /** 13773 * Note: automatically adjusts zoom to keep planet size consistent 13774 * (same size before and after a {@link setLocationAtPoint} call). 13775 */ 13776 setLocationAtPoint(lnglat, point, _elevation) { 13777 const vecToPixelCurrent = angularCoordinatesToSurfaceVector(this.unprojectScreenPoint(point)); 13778 const vecToTarget = angularCoordinatesToSurfaceVector(lnglat); 13779 const zero = createVec3f64(); 13780 zero$1(zero); 13781 const rotatedPixelVector = createVec3f64(); 13782 rotateY(rotatedPixelVector, vecToPixelCurrent, zero, -this.center.lng * Math.PI / 180); 13783 rotateX$1(rotatedPixelVector, rotatedPixelVector, zero, this.center.lat * Math.PI / 180); 13784 const vecToTargetXZLengthSquared = vecToTarget[0] * vecToTarget[0] + vecToTarget[2] * vecToTarget[2]; 13785 const targetXSquared = rotatedPixelVector[0] * rotatedPixelVector[0]; 13786 if (vecToTargetXZLengthSquared < targetXSquared) return; 13787 const intersectionA = Math.sqrt(vecToTargetXZLengthSquared - targetXSquared); 13788 const intersectionB = -intersectionA; 13789 const lngA = angleToRotateBetweenVectors2D(vecToTarget[0], vecToTarget[2], rotatedPixelVector[0], intersectionA); 13790 const lngB = angleToRotateBetweenVectors2D(vecToTarget[0], vecToTarget[2], rotatedPixelVector[0], intersectionB); 13791 const vecToTargetLngA = createVec3f64(); 13792 rotateY(vecToTargetLngA, vecToTarget, zero, -lngA); 13793 const latA = angleToRotateBetweenVectors2D(vecToTargetLngA[1], vecToTargetLngA[2], rotatedPixelVector[1], rotatedPixelVector[2]); 13794 const vecToTargetLngB = createVec3f64(); 13795 rotateY(vecToTargetLngB, vecToTarget, zero, -lngB); 13796 const latB = angleToRotateBetweenVectors2D(vecToTargetLngB[1], vecToTargetLngB[2], rotatedPixelVector[1], rotatedPixelVector[2]); 13797 const limit = Math.PI * .5; 13798 const isValidA = latA >= -limit && latA <= limit; 13799 const isValidB = latB >= -limit && latB <= limit; 13800 let validLng; 13801 let validLat; 13802 if (isValidA && isValidB) { 13803 const centerLngRadians = this.center.lng * Math.PI / 180; 13804 const centerLatRadians = this.center.lat * Math.PI / 180; 13805 const lngDistA = distanceOfAnglesRadians(lngA, centerLngRadians); 13806 const latDistA = distanceOfAnglesRadians(latA, centerLatRadians); 13807 const lngDistB = distanceOfAnglesRadians(lngB, centerLngRadians); 13808 const latDistB = distanceOfAnglesRadians(latB, centerLatRadians); 13809 if (lngDistA + latDistA < lngDistB + latDistB) { 13810 validLng = lngA; 13811 validLat = latA; 13812 } else { 13813 validLng = lngB; 13814 validLat = latB; 13815 } 13816 } else if (isValidA) { 13817 validLng = lngA; 13818 validLat = latA; 13819 } else if (isValidB) { 13820 validLng = lngB; 13821 validLat = latB; 13822 } else return; 13823 const newLng = validLng / Math.PI * 180; 13824 const newLat = validLat / Math.PI * 180; 13825 const oldLat = this.center.lat; 13826 this.setCenter(new LngLat(newLng, clamp(newLat, -90, 90))); 13827 this.setZoom(this.zoom + getZoomAdjustment(oldLat, this.center.lat)); 13828 } 13829 locationToScreenPoint(lnglat, terrain) { 13830 const pos = angularCoordinatesToSurfaceVector(lnglat); 13831 if (terrain) { 13832 const elevation = terrain.getElevationForLngLatZoom(lnglat, this._helper._tileZoom); 13833 scale$1(pos, pos, 1 + elevation / earthRadius); 13834 } 13835 return this._projectSurfacePointToScreen(pos); 13836 } 13837 /** 13838 * Projects a given vector on the surface of a unit sphere (or possible above the surface) 13839 * and returns its coordinates on screen in pixels. 13840 */ 13841 _projectSurfacePointToScreen(pos) { 13842 const projected = createVec4f64(); 13843 transformMat4(projected, [...pos, 1], this._globeViewProjMatrixF64); 13844 projected[0] /= projected[3]; 13845 projected[1] /= projected[3]; 13846 return new Point((projected[0] * .5 + .5) * this.width, (-projected[1] * .5 + .5) * this.height); 13847 } 13848 screenPointToMercatorCoordinate(p, terrain) { 13849 if (terrain) { 13850 const coordinate = this.screenTerrainPointToMercatorCoordinate(p, terrain); 13851 if (coordinate) return coordinate; 13852 } 13853 return MercatorCoordinate.fromLngLat(this.unprojectScreenPoint(p)); 13854 } 13855 /** {@inheritDoc ITransform.screenTerrainPointToMercatorCoordinate} */ 13856 screenTerrainPointToMercatorCoordinate(p, terrain) { 13857 const index = terrain.getCoverageIndex(); 13858 if (!index) return null; 13859 const origin = this.cameraPosition; 13860 const direction = this.getRayDirectionFromPixel(p); 13861 const outer = raySphereIntersection(origin, direction, 1 + index.maxElevation / earthRadius); 13862 if (!outer) return null; 13863 const inner = raySphereIntersection(origin, direction, 1 + index.minElevation / earthRadius); 13864 const tStart = Math.max(outer.tMin, 0); 13865 const tEnd = inner ? inner.tMin : outer.tMax; 13866 if (tEnd <= tStart) return null; 13867 const ray = { 13868 index, 13869 exaggeration: terrain.exaggeration, 13870 origin, 13871 direction 13872 }; 13873 let previousT = 0; 13874 for (let i = 0; i <= GLOBE_SAMPLES; i++) { 13875 const t = tStart + (tEnd - tStart) * i / GLOBE_SAMPLES; 13876 if (globeIsBelowTerrain(ray, t)) { 13877 const { hi } = bisect(ray, globeIsBelowTerrain, previousT, t, GLOBE_BISECT_EPSILON_T); 13878 const { sample, mercator } = globeSampleAt(ray, hi); 13879 return new MercatorCoordinate(mercator.x, mercator.y, sample.elevation); 13880 } 13881 previousT = t; 13882 } 13883 return null; 13884 } 13885 screenPointToLocation(p, terrain) { 13886 return this.screenPointToMercatorCoordinate(p, terrain)?.toLngLat(); 13887 } 13888 screenPointToLocationAtElevation(p, _elevation) { 13889 return this.screenPointToLocation(p); 13890 } 13891 isPointOnMapSurface(p, _terrain) { 13892 const rayOrigin = this._cameraPosition; 13893 return !!raySphereIntersection(rayOrigin, this.getRayDirectionFromPixel(p)); 13894 } 13895 /** 13896 * Computes normalized direction of a ray from the camera to the given screen pixel. 13897 */ 13898 getRayDirectionFromPixel(p) { 13899 const pos = createVec4f64(); 13900 pos[0] = p.x / this.width * 2 - 1; 13901 pos[1] = (p.y / this.height * 2 - 1) * -1; 13902 pos[2] = 1; 13903 pos[3] = 1; 13904 transformMat4(pos, pos, this._globeViewProjMatrixF64Inverted); 13905 pos[0] /= pos[3]; 13906 pos[1] /= pos[3]; 13907 pos[2] /= pos[3]; 13908 const ray = createVec3f64(); 13909 ray[0] = pos[0] - this._cameraPosition[0]; 13910 ray[1] = pos[1] - this._cameraPosition[1]; 13911 ray[2] = pos[2] - this._cameraPosition[2]; 13912 const rayNormalized = createVec3f64(); 13913 normalize(rayNormalized, ray); 13914 return rayNormalized; 13915 } 13916 /** 13917 * For a given point on the unit sphere of the planet, or raised above it, returns whether it lies on the camera's 13918 * side of the horizon plane, the plane the globe shaders clip with (not taking into account camera rotation at all). 13919 */ 13920 isSurfacePointVisible(p) { 13921 const plane = this._cachedClippingPlane; 13922 return plane[0] * p[0] + plane[1] * p[1] + plane[2] * p[2] + plane[3] >= 0; 13923 } 13924 /** 13925 * Returns whether surface point is visible on screen. 13926 * It must both project to a pixel in screen bounds and not be occluded by the planet. 13927 */ 13928 isSurfacePointOnScreen(vec) { 13929 if (!this.isSurfacePointVisible(vec)) return false; 13930 const projected = createVec4f64(); 13931 transformMat4(projected, [...vec, 1], this._globeViewProjMatrixF64); 13932 projected[0] /= projected[3]; 13933 projected[1] /= projected[3]; 13934 projected[2] /= projected[3]; 13935 return projected[0] > -1 && projected[0] < 1 && projected[1] > -1 && projected[1] < 1 && projected[2] > -1 && projected[2] < 1; 13936 } 13937 /** 13938 * @internal 13939 * Returns a {@link LngLat} representing geographical coordinates that correspond to the specified pixel coordinates. 13940 * Note: if the point does not lie on the globe, returns a location on the visible globe horizon (edge) that is 13941 * as close to the point as possible. 13942 * @param p - Screen point in pixels to unproject. 13943 * @param terrain - Optional terrain. 13944 */ 13945 unprojectScreenPoint(p) { 13946 const rayOrigin = this._cameraPosition; 13947 const rayDirection = this.getRayDirectionFromPixel(p); 13948 const intersection = raySphereIntersection(rayOrigin, rayDirection); 13949 if (intersection) { 13950 const intersectionPoint = createVec3f64(); 13951 add(intersectionPoint, rayOrigin, [ 13952 rayDirection[0] * intersection.tMin, 13953 rayDirection[1] * intersection.tMin, 13954 rayDirection[2] * intersection.tMin 13955 ]); 13956 const sphereSurface = createVec3f64(); 13957 normalize(sphereSurface, intersectionPoint); 13958 return sphereSurfacePointToCoordinates(sphereSurface); 13959 } 13960 const horizonPlane = this._cachedClippingPlane; 13961 const directionDotPlaneXyz = horizonPlane[0] * rayDirection[0] + horizonPlane[1] * rayDirection[1] + horizonPlane[2] * rayDirection[2]; 13962 const distanceToIntersection = -pointPlaneSignedDistance(horizonPlane, rayOrigin) / directionDotPlaneXyz; 13963 const maxRayLength = 2; 13964 const planeIntersection = createVec3f64(); 13965 if (distanceToIntersection > 0) add(planeIntersection, rayOrigin, [ 13966 rayDirection[0] * distanceToIntersection, 13967 rayDirection[1] * distanceToIntersection, 13968 rayDirection[2] * distanceToIntersection 13969 ]); 13970 else { 13971 const distantPoint = createVec3f64(); 13972 add(distantPoint, rayOrigin, [ 13973 rayDirection[0] * maxRayLength, 13974 rayDirection[1] * maxRayLength, 13975 rayDirection[2] * maxRayLength 13976 ]); 13977 const distanceFromPlane = pointPlaneSignedDistance(this._cachedClippingPlane, distantPoint); 13978 sub(planeIntersection, distantPoint, [ 13979 this._cachedClippingPlane[0] * distanceFromPlane, 13980 this._cachedClippingPlane[1] * distanceFromPlane, 13981 this._cachedClippingPlane[2] * distanceFromPlane 13982 ]); 13983 } 13984 const horizonDisk = horizonPlaneToCenterAndRadius(horizonPlane); 13985 return sphereSurfacePointToCoordinates(clampToSphere(horizonDisk.center, horizonDisk.radius, planeIntersection)); 13986 } 13987 getProjectionDataForCustomLayer(applyGlobeMatrix = true) { 13988 const globeData = this.getProjectionData({ 13989 overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0), 13990 applyGlobeMatrix 13991 }); 13992 globeData.tileMercatorCoords = [ 13993 0, 13994 0, 13995 1, 13996 1 13997 ]; 13998 return globeData; 13999 } 14000 getFastPathSimpleProjectionMatrix(_tileID) {} 14001}; 14002function globeSampleAt(ray, t) { 14003 const position = createVec3f64(); 14004 scaleAndAdd(position, ray.origin, ray.direction, t); 14005 const radius = length(position); 14006 const surface = createVec3f64(); 14007 scale$1(surface, position, 1 / radius); 14008 const lngLat = sphereSurfacePointToCoordinates(surface); 14009 const projected = MercatorCoordinate.fromLngLat(lngLat); 14010 const mercator = new MercatorCoordinate(projected.x, clamp(projected.y, 0, MAX_MERCATOR_Y)); 14011 const sample = sampleAt(ray.index, ray.exaggeration, mercator.x, mercator.y); 14012 const elevation = Math.abs(lngLat.lat) > 85.051129 ? 0 : sample.elevation; 14013 return { 14014 sample: { 14015 ...sample, 14016 elevation 14017 }, 14018 radius, 14019 mercator 14020 }; 14021} 14022function globeIsBelowTerrain(ray, t) { 14023 const { sample, radius } = globeSampleAt(ray, t); 14024 return isBelowTerrainSample(sample, (radius - 1) * earthRadius); 14025} 14026/** 14027* The location as a vector from the globe's center in globe radii, raised `elevation` meters above the surface. 14028*/ 14029function raisedSurfaceVector(lngLat, elevation) { 14030 const vector = angularCoordinatesToSurfaceVector(lngLat); 14031 return scale$1(vector, vector, 1 + elevation / earthRadius); 14032} 14033/** 14034* Where the point of the ray closest to `point` lies along it, in units of `direction` from `origin`. 14035*/ 14036function rayParameter(origin, direction, point) { 14037 const offset = subtract(createVec3f64(), point, origin); 14038 return dot(offset, direction); 14039} 14040//#endregion 14041//#region src/geo/projection/globe_transform.ts 14042/** 14043* Globe transform is a transform that moves between vertical perspective and mercator projections. 14044* 14045* The two child transforms share this transform's {@link TransformHelper}, so the camera exists exactly once: 14046* a child that moves itself - as {@link setLocationAtPoint} has it do, since only the child knows its own 14047* projection's geometry - has moved this transform too, with no copying back. The children own only what they 14048* derive from that shared camera, such as their matrices, and this transform drives their `_calcMatrices` 14049* because a helper has a single `calcMatrices` callback. 14050*/ 14051var GlobeTransform = class GlobeTransform { 14052 get pixelsToClipSpaceMatrix() { 14053 return this._helper.pixelsToClipSpaceMatrix; 14054 } 14055 get clipSpaceToPixelsMatrix() { 14056 return this._helper.clipSpaceToPixelsMatrix; 14057 } 14058 get pixelsToGLUnits() { 14059 return this._helper.pixelsToGLUnits; 14060 } 14061 get centerOffset() { 14062 return this._helper.centerOffset; 14063 } 14064 get size() { 14065 return this._helper.size; 14066 } 14067 get rotationMatrix() { 14068 return this._helper.rotationMatrix; 14069 } 14070 get centerPoint() { 14071 return this._helper.centerPoint; 14072 } 14073 get pixelsPerMeter() { 14074 return this._helper.pixelsPerMeter; 14075 } 14076 setMinZoom(zoom) { 14077 this._helper.setMinZoom(zoom); 14078 } 14079 setMaxZoom(zoom) { 14080 this._helper.setMaxZoom(zoom); 14081 } 14082 setMinPitch(pitch) { 14083 this._helper.setMinPitch(pitch); 14084 } 14085 setMaxPitch(pitch) { 14086 this._helper.setMaxPitch(pitch); 14087 } 14088 setRenderWorldCopies(renderWorldCopies) { 14089 this._helper.setRenderWorldCopies(renderWorldCopies); 14090 } 14091 setBearing(bearing) { 14092 this._helper.setBearing(bearing); 14093 } 14094 setPitch(pitch) { 14095 this._helper.setPitch(pitch); 14096 } 14097 setRoll(roll) { 14098 this._helper.setRoll(roll); 14099 } 14100 setFov(fov) { 14101 this._helper.setFov(fov); 14102 } 14103 setZoom(zoom) { 14104 this._helper.setZoom(zoom); 14105 } 14106 setCenter(center) { 14107 this._helper.setCenter(center); 14108 } 14109 setElevation(elevation) { 14110 this._helper.setElevation(elevation); 14111 } 14112 setMinElevationForCurrentTile(elevation) { 14113 this._helper.setMinElevationForCurrentTile(elevation); 14114 } 14115 setPadding(padding) { 14116 this._helper.setPadding(padding); 14117 } 14118 interpolatePadding(start, target, t) { 14119 this._helper.interpolatePadding(start, target, t); 14120 } 14121 isPaddingEqual(padding) { 14122 return this._helper.isPaddingEqual(padding); 14123 } 14124 resize(width, height, constrainTransform = true) { 14125 this._helper.resize(width, height, constrainTransform); 14126 } 14127 getMaxBounds() { 14128 return this._helper.getMaxBounds(); 14129 } 14130 setMaxBounds(bounds) { 14131 this._helper.setMaxBounds(bounds); 14132 } 14133 setConstrainOverride(constrain) { 14134 this._helper.setConstrainOverride(constrain); 14135 } 14136 overrideNearFarZ(nearZ, farZ) { 14137 this._helper.overrideNearFarZ(nearZ, farZ); 14138 } 14139 clearNearFarZOverride() { 14140 this._helper.clearNearFarZOverride(); 14141 } 14142 getCameraQueryGeometry(queryGeometry) { 14143 return this._helper.getCameraQueryGeometry(this.getCameraPoint(), queryGeometry); 14144 } 14145 get tileSize() { 14146 return this._helper.tileSize; 14147 } 14148 get tileZoom() { 14149 return this._helper.tileZoom; 14150 } 14151 get scale() { 14152 return this._helper.scale; 14153 } 14154 get worldSize() { 14155 return this._helper.worldSize; 14156 } 14157 get width() { 14158 return this._helper.width; 14159 } 14160 get height() { 14161 return this._helper.height; 14162 } 14163 get lngRange() { 14164 return this._helper.lngRange; 14165 } 14166 get latRange() { 14167 return this._helper.latRange; 14168 } 14169 get minZoom() { 14170 return this._helper.minZoom; 14171 } 14172 get maxZoom() { 14173 return this._helper.maxZoom; 14174 } 14175 get zoom() { 14176 return this._helper.zoom; 14177 } 14178 get center() { 14179 return this._helper.center; 14180 } 14181 get minPitch() { 14182 return this._helper.minPitch; 14183 } 14184 get maxPitch() { 14185 return this._helper.maxPitch; 14186 } 14187 get pitch() { 14188 return this._helper.pitch; 14189 } 14190 get pitchInRadians() { 14191 return this._helper.pitchInRadians; 14192 } 14193 get roll() { 14194 return this._helper.roll; 14195 } 14196 get rollInRadians() { 14197 return this._helper.rollInRadians; 14198 } 14199 get bearing() { 14200 return this._helper.bearing; 14201 } 14202 get bearingInRadians() { 14203 return this._helper.bearingInRadians; 14204 } 14205 get fov() { 14206 return this._helper.fov; 14207 } 14208 get fovInRadians() { 14209 return this._helper.fovInRadians; 14210 } 14211 get elevation() { 14212 return this._helper.elevation; 14213 } 14214 get minElevationForCurrentTile() { 14215 return this._helper.minElevationForCurrentTile; 14216 } 14217 get padding() { 14218 return this._helper.padding; 14219 } 14220 get unmodified() { 14221 return this._helper.unmodified; 14222 } 14223 get renderWorldCopies() { 14224 return this._helper.renderWorldCopies; 14225 } 14226 get cameraToCenterDistance() { 14227 return this._helper.cameraToCenterDistance; 14228 } 14229 get constrainOverride() { 14230 return this._helper.constrainOverride; 14231 } 14232 get nearZ() { 14233 return this._helper.nearZ; 14234 } 14235 get farZ() { 14236 return this._helper.farZ; 14237 } 14238 get autoCalculateNearFarZ() { 14239 return this._helper.autoCalculateNearFarZ; 14240 } 14241 /** 14242 * True when globe render path should be used instead of the old but simpler mercator rendering. 14243 * Globe automatically transitions to mercator at high zoom levels, which causes a switch from 14244 * globe to mercator render path. 14245 */ 14246 get isGlobeRendering() { 14247 return this._globeness > 0; 14248 } 14249 setTransitionState(globeness) { 14250 this._globeness = globeness; 14251 this._calcMatrices(); 14252 this._verticalPerspectiveTransform.getCoveringTilesDetailsProvider().prepareNextFrame(); 14253 this._mercatorTransform.getCoveringTilesDetailsProvider().prepareNextFrame(); 14254 } 14255 get currentTransform() { 14256 return this.isGlobeRendering ? this._verticalPerspectiveTransform : this._mercatorTransform; 14257 } 14258 constructor(options) { 14259 this._globeness = 1; 14260 this.defaultConstrain = (lngLat, zoom) => { 14261 return this.currentTransform.defaultConstrain(lngLat, zoom); 14262 }; 14263 this.applyConstrain = (lngLat, zoom) => { 14264 return this._helper.applyConstrain(lngLat, zoom); 14265 }; 14266 this._helper = new TransformHelper({ 14267 calcMatrices: () => this._calcMatrices(), 14268 defaultConstrain: (center, zoom) => { 14269 return this.defaultConstrain(center, zoom); 14270 } 14271 }, options); 14272 this._globeness = 1; 14273 this._mercatorTransform = new MercatorTransform(void 0, this._helper); 14274 this._verticalPerspectiveTransform = new VerticalPerspectiveTransform(void 0, this._helper); 14275 } 14276 clone() { 14277 const clone = new GlobeTransform(); 14278 clone._globeness = this._globeness; 14279 clone.apply(this, false); 14280 return clone; 14281 } 14282 apply(that, constrain) { 14283 this._helper.apply(that, constrain); 14284 } 14285 get projectionMatrix() { 14286 return this.currentTransform.projectionMatrix; 14287 } 14288 get modelViewProjectionMatrix() { 14289 return this.currentTransform.modelViewProjectionMatrix; 14290 } 14291 get inverseProjectionMatrix() { 14292 return this.currentTransform.inverseProjectionMatrix; 14293 } 14294 get cameraPosition() { 14295 return this.currentTransform.cameraPosition; 14296 } 14297 getProjectionData(params) { 14298 const mercatorProjectionData = this._mercatorTransform.getProjectionData(params); 14299 const verticalPerspectiveProjectionData = this._verticalPerspectiveTransform.getProjectionData(params); 14300 return { 14301 mainMatrix: this.isGlobeRendering ? verticalPerspectiveProjectionData.mainMatrix : mercatorProjectionData.mainMatrix, 14302 clippingPlane: verticalPerspectiveProjectionData.clippingPlane, 14303 tileMercatorCoords: verticalPerspectiveProjectionData.tileMercatorCoords, 14304 projectionTransition: params.applyGlobeMatrix ? this._globeness : 0, 14305 fallbackMatrix: mercatorProjectionData.fallbackMatrix, 14306 clipAntimeridian: verticalPerspectiveProjectionData.clipAntimeridian 14307 }; 14308 } 14309 /** {@inheritDoc ITransform.isLocationOccluded} */ 14310 isLocationOccluded(lngLat, terrain, elevation) { 14311 return this.currentTransform.isLocationOccluded(lngLat, terrain, elevation); 14312 } 14313 transformLightDirection(dir) { 14314 return this.currentTransform.transformLightDirection(dir); 14315 } 14316 getPixelScale() { 14317 return lerp(this._mercatorTransform.getPixelScale(), this._verticalPerspectiveTransform.getPixelScale(), this._globeness); 14318 } 14319 getCircleRadiusCorrection() { 14320 return lerp(this._mercatorTransform.getCircleRadiusCorrection(), this._verticalPerspectiveTransform.getCircleRadiusCorrection(), this._globeness); 14321 } 14322 getPitchedTextCorrection(textAnchorX, textAnchorY, tileID) { 14323 const mercatorCorrection = this._mercatorTransform.getPitchedTextCorrection(textAnchorX, textAnchorY, tileID); 14324 const verticalCorrection = this._verticalPerspectiveTransform.getPitchedTextCorrection(textAnchorX, textAnchorY, tileID); 14325 return lerp(mercatorCorrection, verticalCorrection, this._globeness); 14326 } 14327 projectTileCoordinates(x, y, unwrappedTileID, elevation) { 14328 return this.currentTransform.projectTileCoordinates(x, y, unwrappedTileID, elevation); 14329 } 14330 /** 14331 * Both children write their near/far Z into the shared helper, so the order here is what keeps the two render 14332 * paths at the same depth across the globe-to-mercator transition: vertical perspective computes the globe's Z 14333 * first, and while the globe is rendering mercator is made to reuse that result instead of computing its own. 14334 */ 14335 _calcMatrices() { 14336 if (!this._helper._width || !this._helper._height) return; 14337 this._verticalPerspectiveTransform._calcMatrices(); 14338 this._mercatorTransform._calcMatrices(this.autoCalculateNearFarZ && !this.isGlobeRendering); 14339 } 14340 calculateFogMatrix(unwrappedTileID) { 14341 return this.currentTransform.calculateFogMatrix(unwrappedTileID); 14342 } 14343 getVisibleUnwrappedCoordinates(tileID) { 14344 return this.currentTransform.getVisibleUnwrappedCoordinates(tileID); 14345 } 14346 getCameraFrustum() { 14347 return this.currentTransform.getCameraFrustum(); 14348 } 14349 getClippingPlane() { 14350 return this.currentTransform.getClippingPlane(); 14351 } 14352 getCoveringTilesDetailsProvider() { 14353 return this.currentTransform.getCoveringTilesDetailsProvider(); 14354 } 14355 recalculateZoomAndCenter(terrain) { 14356 this.currentTransform.recalculateZoomAndCenter(terrain); 14357 } 14358 maxPitchScaleFactor() { 14359 return this._mercatorTransform.maxPitchScaleFactor(); 14360 } 14361 getCameraPoint() { 14362 return this._helper.getCameraPoint(); 14363 } 14364 /** The camera of the child that renders the current frame. */ 14365 getCameraAltitude() { 14366 return this.currentTransform.getCameraAltitude(); 14367 } 14368 /** See {@link getCameraAltitude}. */ 14369 getCameraLngLat() { 14370 return this.currentTransform.getCameraLngLat(); 14371 } 14372 populateCache(coords) { 14373 this._mercatorTransform.populateCache(coords); 14374 this._verticalPerspectiveTransform.populateCache(coords); 14375 } 14376 getBounds() { 14377 return this.currentTransform.getBounds(); 14378 } 14379 calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch) { 14380 return this._helper.calculateCenterFromCameraLngLatAlt(lngLat, alt, bearing, pitch); 14381 } 14382 /** See {@link getCameraAltitude}. */ 14383 calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo) { 14384 return this.currentTransform.calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo); 14385 } 14386 /** 14387 * Note: automatically adjusts zoom to keep planet size consistent 14388 * (same size before and after a {@link setLocationAtPoint} call). 14389 */ 14390 setLocationAtPoint(lnglat, point, elevation) { 14391 this.currentTransform.setLocationAtPoint(lnglat, point, elevation); 14392 } 14393 locationToScreenPoint(lnglat, terrain) { 14394 return this.currentTransform.locationToScreenPoint(lnglat, terrain); 14395 } 14396 screenPointToMercatorCoordinate(p, terrain) { 14397 return this.currentTransform.screenPointToMercatorCoordinate(p, terrain); 14398 } 14399 /** {@inheritDoc ITransform.screenTerrainPointToMercatorCoordinate} */ 14400 screenTerrainPointToMercatorCoordinate(p, terrain) { 14401 return this.currentTransform.screenTerrainPointToMercatorCoordinate(p, terrain); 14402 } 14403 screenPointToLocation(p, terrain) { 14404 return this.currentTransform.screenPointToLocation(p, terrain); 14405 } 14406 screenPointToLocationAtElevation(p, elevation) { 14407 return this.currentTransform.screenPointToLocationAtElevation(p, elevation); 14408 } 14409 isPointOnMapSurface(p, terrain) { 14410 return this.currentTransform.isPointOnMapSurface(p, terrain); 14411 } 14412 /** 14413 * Computes normalized direction of a ray from the camera to the given screen pixel. 14414 * 14415 * Always answered by the vertical perspective child: the ray is in unit-sphere space, so it is only meaningful 14416 * to the globe camera controls that ask for it, and the mercator transform does not implement it at all. 14417 */ 14418 getRayDirectionFromPixel(p) { 14419 return this._verticalPerspectiveTransform.getRayDirectionFromPixel(p); 14420 } 14421 getProjectionDataForCustomLayer(applyGlobeMatrix = true) { 14422 const mercatorData = this._mercatorTransform.getProjectionDataForCustomLayer(applyGlobeMatrix); 14423 if (!this.isGlobeRendering) return mercatorData; 14424 const globeData = this._verticalPerspectiveTransform.getProjectionDataForCustomLayer(applyGlobeMatrix); 14425 globeData.fallbackMatrix = mercatorData.mainMatrix; 14426 globeData.projectionTransition = this._globeness; 14427 return globeData; 14428 } 14429 getFastPathSimpleProjectionMatrix(tileID) { 14430 return this.currentTransform.getFastPathSimpleProjectionMatrix(tileID); 14431 } 14432}; 14433//#endregion 14434//#region src/geo/projection/vertical_perspective_camera_helper.ts 14435/** 14436* Zoom movement slowing starts when the mouse ray passes further than this above the planet surface, 14437* so a cursor off the globe does not move the map unnaturally. Globe radius is 1, so 0.3 is ~2000 km. 14438*/ 14439const RAY_SURFACE_DISTANCE_FOR_SLOWING_START = .3; 14440/** How sharply zoom movement slows as the mouse ray rises above the planet surface. Lower is more gradual. */ 14441const SLOWING_MULTIPLIER = .5; 14442/** Longitude difference between zoom location and map center at which the blend from exact to heuristic zooming starts, in degrees. */ 14443const INTERPOLATE_TO_HEURISTIC_START_LNG = 45; 14444/** Longitude difference at which the blend to heuristic zooming is complete, in degrees. */ 14445const INTERPOLATE_TO_HEURISTIC_END_LNG = 85; 14446/** Exponent applied to the blend factor: below 1, so the blend leans towards heuristic zooming and flattens as it completes. */ 14447const INTERPOLATE_TO_HEURISTIC_EXPONENT = .25; 14448/** 14449* Distance of the mouse ray from the globe center at which the blend from exact to heuristic zooming 14450* starts. Globe radius is 1, so 1 is a ray grazing the horizon. 14451*/ 14452const INTERPOLATE_TO_HEURISTIC_START_HORIZON = .95; 14453/** Ray distance at which the blend to heuristic zooming is complete. */ 14454const INTERPOLATE_TO_HEURISTIC_END_HORIZON = .999; 14455/** 14456* Globe radius relative to the smaller viewport dimension below which zoom movement near the horizon 14457* starts being inhibited, avoiding unnatural movements when the map is zoomed out a lot. 14458*/ 14459const SLOWING_RADIUS_START = .9; 14460/** Globe radius relative to the smaller viewport dimension at which that inhibition is at full strength. */ 14461const SLOWING_RADIUS_STOP = .5; 14462/** Fraction of the zoom movement that remains once the globe has shrunk to `SLOWING_RADIUS_STOP`. */ 14463const SLOWING_RADIUS_SLOW_FACTOR = .25; 14464/** 14465* @internal 14466*/ 14467var VerticalPerspectiveCameraHelper = class VerticalPerspectiveCameraHelper { 14468 get useGlobeControls() { 14469 return true; 14470 } 14471 handlePanInertia(pan, transform) { 14472 const panCenter = computeGlobePanCenter(pan, transform); 14473 if (Math.abs(panCenter.lng - transform.center.lng) > 180) panCenter.lng = transform.center.lng + 179.5 * Math.sign(panCenter.lng - transform.center.lng); 14474 return { 14475 easingCenter: panCenter, 14476 easingOffset: new Point(0, 0) 14477 }; 14478 } 14479 /** 14480 * Zooms around the pointer. 14481 * 14482 * `setLocationAtPoint` is exact but degenerates when called repeatedly for a 14483 * location whose longitude is far from the center's, or one near the horizon, 14484 * so those cases blend towards a heuristic. 14485 */ 14486 handleMapControlsRollPitchBearingZoom(deltas, tr) { 14487 const zoomPixel = deltas.around; 14488 const zoomLoc = tr.screenPointToLocation(zoomPixel); 14489 if (deltas.bearingDelta) tr.setBearing(tr.bearing + deltas.bearingDelta); 14490 if (deltas.pitchDelta) tr.setPitch(tr.pitch + deltas.pitchDelta); 14491 if (deltas.rollDelta) tr.setRoll(tr.roll + deltas.rollDelta); 14492 const oldZoomPreZoomDelta = tr.zoom; 14493 if (deltas.zoomDelta) tr.setZoom(tr.zoom + deltas.zoomDelta); 14494 const actualZoomDelta = tr.zoom - oldZoomPreZoomDelta; 14495 if (actualZoomDelta === 0) return; 14496 const dLngRaw = differenceOfAnglesDegrees(tr.center.lng, zoomLoc.lng); 14497 const dLng = dLngRaw / (Math.abs(dLngRaw / 180) + 1); 14498 const dLat = differenceOfAnglesDegrees(tr.center.lat, zoomLoc.lat); 14499 const rayDirection = tr.getRayDirectionFromPixel(zoomPixel); 14500 const rayOrigin = tr.cameraPosition; 14501 const distanceToClosestPoint = dot(rayOrigin, rayDirection) * -1; 14502 const closestPoint = createVec3f64(); 14503 add(closestPoint, rayOrigin, [ 14504 rayDirection[0] * distanceToClosestPoint, 14505 rayDirection[1] * distanceToClosestPoint, 14506 rayDirection[2] * distanceToClosestPoint 14507 ]); 14508 const rayDistanceFromGlobeCenter = length(closestPoint); 14509 const distanceFromSurface = rayDistanceFromGlobeCenter - 1; 14510 const distanceFactor = Math.exp(-Math.max(distanceFromSurface - RAY_SURFACE_DISTANCE_FOR_SLOWING_START, 0) * SLOWING_MULTIPLIER); 14511 const interpolationFactorHorizon = remapSaturate(rayDistanceFromGlobeCenter, INTERPOLATE_TO_HEURISTIC_START_HORIZON, INTERPOLATE_TO_HEURISTIC_END_HORIZON, 0, 1); 14512 const radius = getGlobeRadiusPixels(tr.worldSize, tr.center.lat) / Math.min(tr.width, tr.height); 14513 const radiusFactor = remapSaturate(radius, SLOWING_RADIUS_START, SLOWING_RADIUS_STOP, 1, SLOWING_RADIUS_SLOW_FACTOR); 14514 const slowingFactor = Math.min(distanceFactor, lerp(1, radiusFactor, interpolationFactorHorizon)); 14515 const factor = (1 - zoomScale(-actualZoomDelta)) * slowingFactor; 14516 const oldCenterLat = tr.center.lat; 14517 const oldZoom = tr.zoom; 14518 const heuristicCenter = new LngLat(tr.center.lng + dLng * factor, clamp(tr.center.lat + dLat * factor, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE)); 14519 tr.setLocationAtPoint(zoomLoc, zoomPixel); 14520 const exactCenter = tr.center; 14521 const interpolationFactorLongitude = remapSaturate(Math.abs(dLngRaw), INTERPOLATE_TO_HEURISTIC_START_LNG, INTERPOLATE_TO_HEURISTIC_END_LNG, 0, 1); 14522 const heuristicFactor = Math.pow(Math.max(interpolationFactorLongitude, interpolationFactorHorizon), INTERPOLATE_TO_HEURISTIC_EXPONENT); 14523 const lngExactToHeuristic = differenceOfAnglesDegrees(exactCenter.lng, heuristicCenter.lng); 14524 const latExactToHeuristic = differenceOfAnglesDegrees(exactCenter.lat, heuristicCenter.lat); 14525 tr.setCenter(new LngLat(exactCenter.lng + lngExactToHeuristic * heuristicFactor, exactCenter.lat + latExactToHeuristic * heuristicFactor).wrap()); 14526 tr.setZoom(oldZoom + getZoomAdjustment(oldCenterLat, tr.center.lat)); 14527 } 14528 /** 14529 * Pans the globe by rotating it with a single quaternion that brings the grabbed location back 14530 * under the cursor. This stays consistent near and across the poles, where the previous 14531 * bearing-preserving mapping inverted and stalled (#5296). 14532 * @param deltas - The deltas accumulated for this frame. 14533 * @param tr - The transform to pan. 14534 * @param preZoomAroundLoc - The location that was under the cursor before this frame. 14535 */ 14536 handleMapControlsPan(deltas, tr, preZoomAroundLoc) { 14537 if (!deltas.panDelta) return; 14538 versorSetLocationAtPoint(tr, preZoomAroundLoc, tr.isPointOnMapSurface(deltas.around) ? deltas.around : tr.centerPoint, deltas.panDelta); 14539 } 14540 cameraForBoxAndBearing(options, padding, bounds, bearing, tr) { 14541 const result = cameraForBoxAndBearing(options, padding, bounds, bearing, tr); 14542 const xLeft = padding.left / tr.width * 2 - 1; 14543 const xRight = (tr.width - padding.right) / tr.width * 2 - 1; 14544 const yTop = padding.top / tr.height * -2 + 1; 14545 const yBottom = (tr.height - padding.bottom) / tr.height * -2 + 1; 14546 const flipEastWest = differenceOfAnglesDegrees(bounds.getWest(), bounds.getEast()) < 0; 14547 const lngWest = flipEastWest ? bounds.getEast() : bounds.getWest(); 14548 const lngEast = flipEastWest ? bounds.getWest() : bounds.getEast(); 14549 const latNorth = Math.max(bounds.getNorth(), bounds.getSouth()); 14550 const latSouth = Math.min(bounds.getNorth(), bounds.getSouth()); 14551 const lngMid = lngWest + differenceOfAnglesDegrees(lngWest, lngEast) * .5; 14552 const latMid = latNorth + differenceOfAnglesDegrees(latNorth, latSouth) * .5; 14553 const clonedTr = tr.clone(); 14554 clonedTr.setCenter(result.center); 14555 clonedTr.setBearing(result.bearing); 14556 clonedTr.setPitch(0); 14557 clonedTr.setRoll(0); 14558 clonedTr.setZoom(result.zoom); 14559 const matrix = clonedTr.modelViewProjectionMatrix; 14560 const testVectors = [ 14561 angularCoordinatesToSurfaceVector(bounds.getNorthWest()), 14562 angularCoordinatesToSurfaceVector(bounds.getNorthEast()), 14563 angularCoordinatesToSurfaceVector(bounds.getSouthWest()), 14564 angularCoordinatesToSurfaceVector(bounds.getSouthEast()), 14565 angularCoordinatesToSurfaceVector(new LngLat(lngEast, latMid)), 14566 angularCoordinatesToSurfaceVector(new LngLat(lngWest, latMid)), 14567 angularCoordinatesToSurfaceVector(new LngLat(lngMid, latNorth)), 14568 angularCoordinatesToSurfaceVector(new LngLat(lngMid, latSouth)) 14569 ]; 14570 const vecToCenter = angularCoordinatesToSurfaceVector(result.center); 14571 let smallestNeededScale = Number.POSITIVE_INFINITY; 14572 for (const vec of testVectors) { 14573 if (xLeft < 0) smallestNeededScale = VerticalPerspectiveCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, VerticalPerspectiveCameraHelper.solveVectorScale(vec, vecToCenter, matrix, "x", xLeft)); 14574 if (xRight > 0) smallestNeededScale = VerticalPerspectiveCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, VerticalPerspectiveCameraHelper.solveVectorScale(vec, vecToCenter, matrix, "x", xRight)); 14575 if (yTop > 0) smallestNeededScale = VerticalPerspectiveCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, VerticalPerspectiveCameraHelper.solveVectorScale(vec, vecToCenter, matrix, "y", yTop)); 14576 if (yBottom < 0) smallestNeededScale = VerticalPerspectiveCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, VerticalPerspectiveCameraHelper.solveVectorScale(vec, vecToCenter, matrix, "y", yBottom)); 14577 } 14578 if (!Number.isFinite(smallestNeededScale) || smallestNeededScale === 0) { 14579 cameraBoundsWarning(); 14580 return; 14581 } 14582 result.zoom = Math.min(clonedTr.zoom + scaleZoom(smallestNeededScale), options.maxZoom); 14583 return result; 14584 } 14585 /** 14586 * Handles the zoom and center change during camera jumpTo. 14587 */ 14588 handleJumpToCenterZoom(tr, options) { 14589 const startingLat = tr.center.lat; 14590 const constrainedCenter = tr.applyConstrain(options.center ? LngLat.convert(options.center) : tr.center, tr.zoom).center; 14591 tr.setCenter(constrainedCenter.wrap()); 14592 const targetZoom = typeof options.zoom !== "undefined" ? +options.zoom : tr.zoom + getZoomAdjustment(startingLat, constrainedCenter.lat); 14593 if (tr.zoom !== targetZoom) tr.setZoom(targetZoom); 14594 } 14595 handleEaseTo(tr, options) { 14596 const startZoom = tr.zoom; 14597 const startCenter = tr.center; 14598 const startPadding = tr.padding; 14599 const startEulerAngles = { 14600 roll: tr.roll, 14601 pitch: tr.pitch, 14602 bearing: tr.bearing 14603 }; 14604 const endEulerAngles = { 14605 roll: options.roll === void 0 ? tr.roll : options.roll, 14606 pitch: options.pitch === void 0 ? tr.pitch : options.pitch, 14607 bearing: options.bearing === void 0 ? tr.bearing : options.bearing 14608 }; 14609 const optionsZoom = typeof options.zoom !== "undefined"; 14610 const doPadding = !tr.isPaddingEqual(options.padding); 14611 let isZooming = false; 14612 const preConstrainCenter = options.center ? LngLat.convert(options.center) : startCenter; 14613 const constrainedCenter = tr.applyConstrain(preConstrainCenter, startZoom).center; 14614 normalizeCenter(tr, constrainedCenter); 14615 const clonedTr = tr.clone(); 14616 clonedTr.setCenter(constrainedCenter); 14617 clonedTr.setZoom(optionsZoom ? +options.zoom : startZoom + getZoomAdjustment(startCenter.lat, preConstrainCenter.lat)); 14618 clonedTr.setBearing(options.bearing); 14619 const clampedPoint = new Point(clamp(tr.centerPoint.x + options.offsetAsPoint.x, 0, tr.width), clamp(tr.centerPoint.y + options.offsetAsPoint.y, 0, tr.height)); 14620 clonedTr.setLocationAtPoint(constrainedCenter, clampedPoint); 14621 const endCenterWithShift = (options.offset && options.offsetAsPoint.mag()) > 0 ? clonedTr.center : constrainedCenter; 14622 const endZoomWithShift = optionsZoom ? +options.zoom : startZoom + getZoomAdjustment(startCenter.lat, endCenterWithShift.lat); 14623 const normalizedStartZoom = startZoom + getZoomAdjustment(startCenter.lat, 0); 14624 const normalizedEndZoom = endZoomWithShift + getZoomAdjustment(endCenterWithShift.lat, 0); 14625 const deltaLng = differenceOfAnglesDegrees(startCenter.lng, endCenterWithShift.lng); 14626 const deltaLat = differenceOfAnglesDegrees(startCenter.lat, endCenterWithShift.lat); 14627 const finalScale = zoomScale(normalizedEndZoom - normalizedStartZoom); 14628 isZooming = endZoomWithShift !== startZoom; 14629 const easeFunc = (k) => { 14630 if (!rollPitchBearingEqual(startEulerAngles, endEulerAngles)) updateRotation({ 14631 startEulerAngles, 14632 endEulerAngles, 14633 tr, 14634 k, 14635 useSlerp: startEulerAngles.roll != endEulerAngles.roll 14636 }); 14637 if (doPadding) tr.interpolatePadding(startPadding, options.padding, k); 14638 if (options.around) { 14639 warnOnce("Easing around a point is not supported under globe projection."); 14640 tr.setLocationAtPoint(options.around, options.aroundPoint); 14641 } else { 14642 const factor = k * Math.pow(normalizedEndZoom > normalizedStartZoom ? Math.min(2, finalScale) : Math.max(.5, finalScale), 1 - k); 14643 const newCenter = interpolateLngLatForGlobe(startCenter, deltaLng, deltaLat, factor); 14644 tr.setCenter(newCenter.wrap()); 14645 } 14646 if (isZooming) { 14647 const interpolatedZoom = interpolateFactory.number(normalizedStartZoom, normalizedEndZoom, k) + getZoomAdjustment(0, tr.center.lat); 14648 tr.setZoom(interpolatedZoom); 14649 } 14650 }; 14651 return { 14652 easeFunc, 14653 isZooming, 14654 elevationCenter: endCenterWithShift 14655 }; 14656 } 14657 handleFlyTo(tr, options) { 14658 const optionsZoom = typeof options.zoom !== "undefined"; 14659 const startCenter = tr.center; 14660 const startZoom = tr.zoom; 14661 const startPadding = tr.padding; 14662 const doPadding = !tr.isPaddingEqual(options.padding); 14663 const constrainedCenter = tr.applyConstrain(LngLat.convert(options.center || options.locationAtOffset), startZoom).center; 14664 const targetZoom = optionsZoom ? +options.zoom : tr.zoom + getZoomAdjustment(tr.center.lat, constrainedCenter.lat); 14665 const clonedTr = tr.clone(); 14666 clonedTr.setCenter(constrainedCenter); 14667 clonedTr.setZoom(targetZoom); 14668 clonedTr.setBearing(options.bearing); 14669 const clampedPoint = new Point(clamp(tr.centerPoint.x + options.offsetAsPoint.x, 0, tr.width), clamp(tr.centerPoint.y + options.offsetAsPoint.y, 0, tr.height)); 14670 clonedTr.setLocationAtPoint(constrainedCenter, clampedPoint); 14671 const targetCenter = clonedTr.center; 14672 normalizeCenter(tr, targetCenter); 14673 const pixelPathLength = globeDistanceOfLocationsPixels(tr, startCenter, targetCenter); 14674 const normalizedStartZoom = startZoom + getZoomAdjustment(startCenter.lat, 0); 14675 const normalizedTargetZoom = targetZoom + getZoomAdjustment(targetCenter.lat, 0); 14676 const scaleOfZoom = zoomScale(normalizedTargetZoom - normalizedStartZoom); 14677 const requestedMinZoom = typeof options.minZoom === "number" ? +options.minZoom : tr.minZoom; 14678 const normalizedEffectiveMinZoom = Math.max(requestedMinZoom, tr.minZoom) + getZoomAdjustment(targetCenter.lat, 0); 14679 const minZoomPreConstrain = Math.min(normalizedEffectiveMinZoom, normalizedStartZoom, normalizedTargetZoom) + getZoomAdjustment(0, targetCenter.lat); 14680 const normalizedMinZoom = tr.applyConstrain(targetCenter, minZoomPreConstrain).zoom + getZoomAdjustment(targetCenter.lat, 0); 14681 const scaleOfMinZoom = zoomScale(normalizedMinZoom - normalizedStartZoom); 14682 const deltaLng = differenceOfAnglesDegrees(startCenter.lng, targetCenter.lng); 14683 const deltaLat = differenceOfAnglesDegrees(startCenter.lat, targetCenter.lat); 14684 const easeFunc = (k, scale, centerFactor, _pointAtOffset) => { 14685 const interpolatedCenter = interpolateLngLatForGlobe(startCenter, deltaLng, deltaLat, centerFactor); 14686 if (doPadding) tr.interpolatePadding(startPadding, options.padding, k); 14687 const newCenter = k === 1 ? targetCenter : interpolatedCenter; 14688 tr.setCenter(newCenter.wrap()); 14689 const interpolatedZoom = normalizedStartZoom + scaleZoom(scale); 14690 tr.setZoom(k === 1 ? targetZoom : interpolatedZoom + getZoomAdjustment(0, newCenter.lat)); 14691 }; 14692 return { 14693 easeFunc, 14694 scaleOfZoom, 14695 targetCenter, 14696 scaleOfMinZoom, 14697 pixelPathLength 14698 }; 14699 } 14700 /** 14701 * Computes how much to scale the globe in order for a given point on its surface (a location) to project to a given clip space coordinate in either the X or the Y axis. 14702 * @param vector - Position of the queried location on the surface of the unit sphere globe. 14703 * @param toCenter - Position of current transform center on the surface of the unit sphere globe. 14704 * This is needed because zooming the globe not only changes its scale, 14705 * but also moves the camera closer or further away along this vector (pitch is disregarded). 14706 * @param projection - The globe projection matrix. 14707 * @param targetDimension - The dimension in which the scaled vector must match the target value in clip space. 14708 * @param targetValue - The target clip space value in the specified dimension to which the queried vector must project. 14709 * @returns How much to scale the globe. 14710 */ 14711 static solveVectorScale(vector, toCenter, projection, targetDimension, targetValue) { 14712 const k = targetValue; 14713 const columnXorY = targetDimension === "x" ? [ 14714 projection[0], 14715 projection[4], 14716 projection[8], 14717 projection[12] 14718 ] : [ 14719 projection[1], 14720 projection[5], 14721 projection[9], 14722 projection[13] 14723 ]; 14724 const columnZ = [ 14725 projection[3], 14726 projection[7], 14727 projection[11], 14728 projection[15] 14729 ]; 14730 const vecDotXY = vector[0] * columnXorY[0] + vector[1] * columnXorY[1] + vector[2] * columnXorY[2]; 14731 const vecDotZ = vector[0] * columnZ[0] + vector[1] * columnZ[1] + vector[2] * columnZ[2]; 14732 const toCenterDotXY = toCenter[0] * columnXorY[0] + toCenter[1] * columnXorY[1] + toCenter[2] * columnXorY[2]; 14733 const toCenterDotZ = toCenter[0] * columnZ[0] + toCenter[1] * columnZ[1] + toCenter[2] * columnZ[2]; 14734 const t = (toCenterDotXY + columnXorY[3] - k * toCenterDotZ - k * columnZ[3]) / (toCenterDotXY - vecDotXY - k * toCenterDotZ + k * vecDotZ); 14735 if (toCenterDotXY + k * vecDotZ === vecDotXY + k * toCenterDotZ || columnZ[3] * (vecDotXY - toCenterDotXY) + columnXorY[3] * (toCenterDotZ - vecDotZ) + vecDotXY * toCenterDotZ === toCenterDotXY * vecDotZ) return null; 14736 return t; 14737 } 14738 /** 14739 * Returns `newValue` if it is: 14740 * 14741 * - not null AND 14742 * - not negative AND 14743 * - smaller than `newValue`, 14744 * 14745 * ...otherwise returns `oldValue`. 14746 */ 14747 static getLesserNonNegativeNonNull(oldValue, newValue) { 14748 if (newValue !== null && newValue >= 0 && newValue < oldValue) return newValue; 14749 else return oldValue; 14750 } 14751}; 14752//#endregion 14753//#region src/geo/projection/globe_camera_helper.ts 14754/** 14755* @internal 14756*/ 14757var GlobeCameraHelper = class { 14758 constructor(globe) { 14759 this._globe = globe; 14760 this._mercatorCameraHelper = new MercatorCameraHelper(); 14761 this._verticalPerspectiveCameraHelper = new VerticalPerspectiveCameraHelper(); 14762 } 14763 get useGlobeControls() { 14764 return this._globe.useGlobeRendering; 14765 } 14766 get currentHelper() { 14767 return this.useGlobeControls ? this._verticalPerspectiveCameraHelper : this._mercatorCameraHelper; 14768 } 14769 handlePanInertia(pan, transform) { 14770 return this.currentHelper.handlePanInertia(pan, transform); 14771 } 14772 handleMapControlsRollPitchBearingZoom(deltas, tr) { 14773 this.currentHelper.handleMapControlsRollPitchBearingZoom(deltas, tr); 14774 } 14775 handleMapControlsPan(deltas, tr, preZoomAroundLoc) { 14776 this.currentHelper.handleMapControlsPan(deltas, tr, preZoomAroundLoc); 14777 } 14778 cameraForBoxAndBearing(options, padding, bounds, bearing, tr) { 14779 return this.currentHelper.cameraForBoxAndBearing(options, padding, bounds, bearing, tr); 14780 } 14781 /** 14782 * Handles the zoom and center change during camera jumpTo. 14783 */ 14784 handleJumpToCenterZoom(tr, options) { 14785 this.currentHelper.handleJumpToCenterZoom(tr, options); 14786 } 14787 handleEaseTo(tr, options) { 14788 return this.currentHelper.handleEaseTo(tr, options); 14789 } 14790 handleFlyTo(tr, options) { 14791 return this.currentHelper.handleFlyTo(tr, options); 14792 } 14793}; 14794//#endregion 14795//#region src/geo/projection/projection_factory.ts 14796function createProjectionFromName(name, transformConstrain, globalState) { 14797 const transformOptions = { constrainOverride: transformConstrain }; 14798 if (Array.isArray(name)) { 14799 const globeProjection = new GlobeProjection({ type: name }, globalState); 14800 return { 14801 projection: globeProjection, 14802 transform: new GlobeTransform(transformOptions), 14803 cameraHelper: new GlobeCameraHelper(globeProjection) 14804 }; 14805 } 14806 switch (name) { 14807 case "mercator": return { 14808 projection: new MercatorProjection(), 14809 transform: new MercatorTransform(transformOptions), 14810 cameraHelper: new MercatorCameraHelper() 14811 }; 14812 case "globe": { 14813 const globeProjection = new GlobeProjection({ type: [ 14814 "interpolate", 14815 ["linear"], 14816 ["zoom"], 14817 11, 14818 "vertical-perspective", 14819 12, 14820 "mercator" 14821 ] }, {}); 14822 return { 14823 projection: globeProjection, 14824 transform: new GlobeTransform(transformOptions), 14825 cameraHelper: new GlobeCameraHelper(globeProjection) 14826 }; 14827 } 14828 case "vertical-perspective": return { 14829 projection: new VerticalPerspectiveProjection(), 14830 transform: new VerticalPerspectiveTransform(transformOptions), 14831 cameraHelper: new VerticalPerspectiveCameraHelper() 14832 }; 14833 default: 14834 warnOnce(`Unknown projection name: ${name}. Falling back to mercator projection.`); 14835 return { 14836 projection: new MercatorProjection(), 14837 transform: new MercatorTransform(transformOptions), 14838 cameraHelper: new MercatorCameraHelper() 14839 }; 14840 } 14841} 14842//#endregion 14843//#region src/style/style.ts 14844const empty = emptyStyle(); 14845/** 14846* The Style base class 14847*/ 14848var Style = class extends Evented { 14849 constructor(map, options = {}) { 14850 super(); 14851 this._rtlPluginLoaded = () => { 14852 for (const id in this.tileManagers) { 14853 const sourceType = this.tileManagers[id].getSource().type; 14854 if (sourceType === "vector" || sourceType === "geojson") this.tileManagers[id].reload(); 14855 } 14856 }; 14857 this.map = map; 14858 this.dispatcher = new Dispatcher(getGlobalWorkerPool(), map._getMapId()).setEventedParent(this); 14859 this.dispatcher.registerMessageHandler("GG", (mapId, params) => { 14860 return this.getGlyphs(mapId, params); 14861 }); 14862 this.dispatcher.registerMessageHandler("GI", (mapId, params) => { 14863 return this.getImages(mapId, params); 14864 }); 14865 this.dispatcher.registerMessageHandler("GDA", (mapId, params) => { 14866 return this.getDashes(mapId, params); 14867 }); 14868 this.imageManager = new ImageManager(); 14869 this.imageManager.setEventedParent(this); 14870 this.imageManager.setMissingImageResolver(map._missingStyleImageResolver); 14871 this.patternAtlas = new PatternAtlas(this.imageManager); 14872 const glyphLang = map._container?.lang || typeof document !== "undefined" && document.documentElement?.lang || void 0; 14873 this.glyphManager = new GlyphManager(map._requestManager, options.localIdeographFontFamily, glyphLang); 14874 this.lineAtlas = new LineAtlas(256, 512); 14875 this.crossTileSymbolIndex = new CrossTileSymbolIndex(); 14876 this._setInitialValues(); 14877 this._resetUpdates(); 14878 this.dispatcher.broadcast("SR", getReferrer()); 14879 rtlMainThreadPluginFactory().on(RTLPluginLoadedEventName, this._rtlPluginLoaded); 14880 this.on("data", (event) => { 14881 if (event.dataType !== "source" || event.sourceDataType !== "metadata") return; 14882 const tileManager = this.tileManagers[event.sourceId]; 14883 if (!tileManager) return; 14884 const source = tileManager.getSource(); 14885 if (!source?.vectorLayerIds) return; 14886 for (const layerId in this._layers) { 14887 const layer = this._layers[layerId]; 14888 if (layer.source === source.id) this._validateLayer(layer); 14889 } 14890 }); 14891 } 14892 _setInitialValues() { 14893 this._layers = {}; 14894 this._order = []; 14895 this.tileManagers = {}; 14896 this.zoomHistory = new ZoomHistory(); 14897 this._imagesListDirty = false; 14898 this._globalState = {}; 14899 this._serializedLayers = {}; 14900 this.stylesheet = null; 14901 this.light = null; 14902 this.sky = null; 14903 if (this.projection) { 14904 this.projection.destroy(); 14905 delete this.projection; 14906 } 14907 this._loaded = false; 14908 this._changed = false; 14909 this._updatedLayers = {}; 14910 this._updatedSources = {}; 14911 this._changedImages = {}; 14912 this._glyphsDidChange = false; 14913 this._updatedPaintProps = {}; 14914 this._layerOrderChanged = false; 14915 this._symbolPlacementTriggered = false; 14916 this._placedProjectionTransition = void 0; 14917 this.crossTileSymbolIndex = new ((this.crossTileSymbolIndex?.constructor) || Object)(); 14918 this.pauseablePlacement = void 0; 14919 this.placement = void 0; 14920 this.z = 0; 14921 } 14922 setGlobalStateProperty(name, value) { 14923 this._checkLoaded(); 14924 const newValue = value === null ? this.stylesheet.state?.[name]?.default ?? null : value; 14925 this._setGlobalStateValues({ [name]: newValue }); 14926 return this; 14927 } 14928 getGlobalState() { 14929 return this._globalState; 14930 } 14931 setGlobalState(newStylesheetState) { 14932 this._checkLoaded(); 14933 const values = {}; 14934 for (const propertyName in newStylesheetState) values[propertyName] = newStylesheetState[propertyName].default; 14935 this._setGlobalStateValues(values); 14936 } 14937 /** Sets the keys in `values` that differ, keeping a copy of the state from before so readers transition from it. */ 14938 _setGlobalStateValues(values) { 14939 const changedGlobalStateRefs = []; 14940 for (const ref in values) if (!deepEqual(values[ref], this._globalState[ref])) changedGlobalStateRefs.push(ref); 14941 if (changedGlobalStateRefs.length === 0) return; 14942 const priorGlobalState = { ...this._globalState }; 14943 for (const ref of changedGlobalStateRefs) this._globalState[ref] = values[ref]; 14944 this._applyGlobalStateChanges(changedGlobalStateRefs, priorGlobalState); 14945 } 14946 /** 14947 * @internal 14948 * Find all sources that are affected by the global state changes and reload them. 14949 * Find all paint, light and sky properties that are affected and transition them from `priorGlobalState`, the state before the change. 14950 * For example, if a layer filter uses global-state expression, this function will find the source id of that layer. 14951 */ 14952 _applyGlobalStateChanges(globalStateRefs, priorGlobalState) { 14953 const sourceIdsToReload = /* @__PURE__ */ new Set(); 14954 const globalStateChange = {}; 14955 for (const ref of globalStateRefs) globalStateChange[ref] = this._globalState[ref]; 14956 for (const layerId in this._layers) { 14957 const layer = this._layers[layerId]; 14958 layer.retainPriorGlobalState(globalStateRefs, priorGlobalState); 14959 const layoutAffectingGlobalStateRefs = layer.getLayoutAffectingGlobalStateRefs(); 14960 const paintAffectingGlobalStateRefs = layer.getPaintAffectingGlobalStateRefs(); 14961 const visibilityAffectingGlobalStateRefs = layer.getVisibilityAffectingGlobalStateRefs(); 14962 for (const ref of globalStateRefs) { 14963 if (layoutAffectingGlobalStateRefs.has(ref)) sourceIdsToReload.add(layer.source); 14964 if (paintAffectingGlobalStateRefs.has(ref)) for (const { name, value } of paintAffectingGlobalStateRefs.get(ref)) this._updatePaintProperty(layer, name, value, { validate: false }); 14965 if (visibilityAffectingGlobalStateRefs?.has(ref)) { 14966 layer.recalculateVisibility(); 14967 this._updateLayer(layer); 14968 } 14969 } 14970 } 14971 const parameters = { 14972 now: now(), 14973 transition: this.getTransition() 14974 }; 14975 this.light?.applyGlobalStateChange(globalStateRefs, priorGlobalState, parameters); 14976 this.sky?.applyGlobalStateChange(globalStateRefs, priorGlobalState, parameters); 14977 this.dispatcher.broadcast("UGS", globalStateChange); 14978 for (const id in this.tileManagers) if (sourceIdsToReload.has(id)) { 14979 this._reloadSource(id); 14980 this._changed = true; 14981 } 14982 } 14983 async loadURL(url, options = {}, previousStyle) { 14984 this.fire(new MapStyleDataEvent("dataloading")); 14985 options.validate = typeof options.validate === "boolean" ? options.validate : true; 14986 this._loadStyleRequest = new AbortController(); 14987 const abortController = this._loadStyleRequest; 14988 try { 14989 const request = await this.map._requestManager.transformRequest(url, "Style"); 14990 throwIfAborted(abortController.signal); 14991 const response = await getJSON(request, abortController); 14992 if (this._loadStyleRequest === abortController) this._loadStyleRequest = null; 14993 this._load(response.data, options, previousStyle); 14994 } catch (error) { 14995 if (this._loadStyleRequest === abortController) this._loadStyleRequest = null; 14996 if (error && !abortController.signal.aborted) this.fire(new ErrorEvent(ensureError(error))); 14997 } 14998 } 14999 loadJSON(json, options = {}, previousStyle) { 15000 this.fire(new MapStyleDataEvent("dataloading")); 15001 this._frameRequest = new AbortController(); 15002 browser.frameAsync(this._frameRequest, this.map._ownerWindow).then(() => { 15003 this._frameRequest = null; 15004 options.validate = options.validate !== false; 15005 this._load(json, options, previousStyle); 15006 }).catch(() => {}); 15007 } 15008 loadEmpty() { 15009 this.fire(new MapStyleDataEvent("dataloading")); 15010 this._load(empty, { validate: false }); 15011 } 15012 _load(json, options, previousStyle) { 15013 let nextState = options.transformStyle ? options.transformStyle(previousStyle, json) : json; 15014 if (options.validate && validateStyleAndEmit(this, nextState)) return; 15015 nextState = { ...nextState }; 15016 this._loaded = true; 15017 this.stylesheet = nextState; 15018 for (const id in nextState.sources) this.addSource(id, nextState.sources[id], { validate: false }); 15019 if (nextState.sprite) this._loadSprite(nextState.sprite); 15020 else this.imageManager.setLoaded(true); 15021 this.glyphManager.setURL(nextState.glyphs); 15022 this.glyphManager.setFontFaces(nextState["font-faces"]); 15023 this._createLayers(); 15024 this.light = new Light(this.stylesheet.light ?? {}, this._globalState); 15025 this._setProjectionInternal(this.stylesheet.projection?.type || "mercator"); 15026 this.sky = new Sky(this.stylesheet.sky, this._globalState); 15027 this.sky.setEventedParent(this); 15028 this.map.setTerrain(this.stylesheet.terrain ?? null, { validate: false }); 15029 this.fire(new MapStyleDataEvent("data")); 15030 this.fire(new MapStyleLoadEvent()); 15031 } 15032 _createLayers() { 15033 const dereferencedLayers = derefLayers(this.stylesheet.layers); 15034 this.setGlobalState(this.stylesheet.state ?? null); 15035 this.dispatcher.broadcast("SL", dereferencedLayers); 15036 this._order = dereferencedLayers.map((layer) => layer.id); 15037 this._layers = {}; 15038 this._serializedLayers = null; 15039 for (const layer of dereferencedLayers) { 15040 const styledLayer = createStyleLayer(layer, this._globalState); 15041 styledLayer.setEventedParent(this, { layer: { id: layer.id } }); 15042 this._layers[layer.id] = styledLayer; 15043 if (isRasterStyleLayer(styledLayer) && this.tileManagers[styledLayer.source]) { 15044 const rasterFadeDuration = layer.paint?.["raster-fade-duration"] ?? styledLayer.paint.get("raster-fade-duration"); 15045 this.tileManagers[styledLayer.source].setRasterFadeDuration(rasterFadeDuration); 15046 } 15047 } 15048 } 15049 async _loadSprite(sprite, isUpdate = false, completion = void 0) { 15050 this.imageManager.setLoaded(false); 15051 const abortController = new AbortController(); 15052 this._spriteRequest = abortController; 15053 let err; 15054 try { 15055 const images = await loadSprite(sprite, this.map._requestManager, this.map.getPixelRatio(), abortController); 15056 if (!images) return; 15057 for (const spriteId in images) { 15058 const { loaded, removed } = this.imageManager.setSpriteImages(spriteId, images[spriteId]); 15059 this._markImagesChanged(removed); 15060 if (isUpdate) this._markImagesChanged(loaded); 15061 } 15062 } catch (error) { 15063 err = error; 15064 if (!abortController.signal.aborted) this.fire(new ErrorEvent(err)); 15065 } finally { 15066 this._spriteRequest = null; 15067 this.imageManager.setLoaded(true); 15068 if (isUpdate) this._changed = true; 15069 this.dispatcher.broadcast("SI", this.imageManager.listImages()); 15070 this.fire(new MapStyleDataEvent("data")); 15071 completion?.(err); 15072 } 15073 } 15074 _unloadSprite() { 15075 this._markImagesChanged(this.imageManager.removeAllSpriteImages()); 15076 this._imagesListDirty = true; 15077 this._changed = true; 15078 this.fire(new MapStyleDataEvent("data")); 15079 } 15080 _validateLayer(layer) { 15081 const tileManager = this.tileManagers[layer.source]; 15082 if (!tileManager) return; 15083 const sourceLayer = layer.sourceLayer; 15084 if (!sourceLayer) return; 15085 const source = tileManager.getSource(); 15086 if (source.type === "geojson" || source.vectorLayerIds && !source.vectorLayerIds.includes(sourceLayer)) this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Source layer "${sourceLayer}" does not exist on source "${source.id}" as specified by style layer "${layer.id}".`))); 15087 } 15088 loaded() { 15089 if (!this._loaded) return false; 15090 if (Object.keys(this._updatedSources).length) return false; 15091 for (const id in this.tileManagers) if (!this.tileManagers[id].loaded()) return false; 15092 return this.imageManager.isLoaded(); 15093 } 15094 /** 15095 * @hidden 15096 * take an array of string IDs, and based on this._layers, generate an array of LayerSpecification 15097 * @param ids - an array of string IDs, for which serialized layers will be generated. If omitted, all serialized layers will be returned 15098 * @param returnClone - if true, return a clone of the layer object 15099 * @returns generated result 15100 */ 15101 _serializeByIds(ids, returnClone = false) { 15102 const serializedLayersDictionary = this._serializedAllLayers(); 15103 if (!ids || ids.length === 0) return returnClone ? Object.values(clone$2(serializedLayersDictionary)) : Object.values(serializedLayersDictionary); 15104 const serializedLayers = []; 15105 for (const id of ids) if (serializedLayersDictionary[id]) { 15106 const toPush = returnClone ? clone$2(serializedLayersDictionary[id]) : serializedLayersDictionary[id]; 15107 serializedLayers.push(toPush); 15108 } 15109 return serializedLayers; 15110 } 15111 /** 15112 * @hidden 15113 * Lazy initialization of this._serializedLayers dictionary and return it 15114 * @returns this._serializedLayers dictionary 15115 */ 15116 _serializedAllLayers() { 15117 let serializedLayers = this._serializedLayers; 15118 if (serializedLayers) return serializedLayers; 15119 serializedLayers = this._serializedLayers = {}; 15120 const allLayerIds = Object.keys(this._layers); 15121 for (const layerId of allLayerIds) { 15122 const layer = this._layers[layerId]; 15123 if (layer.type !== "custom") serializedLayers[layerId] = layer.serialize(); 15124 } 15125 return serializedLayers; 15126 } 15127 hasTransitions() { 15128 if (this.light?.hasTransition()) return true; 15129 if (this.sky?.hasTransition()) return true; 15130 if (this.projection?.hasTransition()) return true; 15131 for (const id in this.tileManagers) if (this.tileManagers[id].hasTransition()) return true; 15132 for (const id in this._layers) if (this._layers[id].hasTransition()) return true; 15133 return false; 15134 } 15135 _checkLoaded() { 15136 if (!this._loaded) throw new Error("Style is not done loading."); 15137 } 15138 /** 15139 * @internal 15140 * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. 15141 */ 15142 update(parameters) { 15143 if (!this._loaded) return; 15144 const changed = this._changed; 15145 if (changed) { 15146 if (this._imagesListDirty) { 15147 this.dispatcher.broadcast("SI", this.imageManager.listImages()); 15148 this._imagesListDirty = false; 15149 } 15150 const updatedIds = Object.keys(this._updatedLayers); 15151 const removedIds = Object.keys(this._removedLayers); 15152 if (updatedIds.length || removedIds.length) this._updateWorkerLayers(updatedIds, removedIds); 15153 for (const id in this._updatedSources) { 15154 const action = this._updatedSources[id]; 15155 if (action === "reload") this._reloadSource(id); 15156 else if (action === "clear") this._clearSource(id); 15157 else throw new Error(`Invalid action ${action}`); 15158 } 15159 this._updateTilesForChangedImages(); 15160 this._updateTilesForChangedGlyphs(); 15161 for (const id in this._updatedPaintProps) this._layers[id].updateTransitions(parameters); 15162 this._resetUpdates(); 15163 } 15164 const managersUsedBefore = {}; 15165 for (const id in this.tileManagers) { 15166 const tileManager = this.tileManagers[id]; 15167 managersUsedBefore[id] = tileManager.used; 15168 tileManager.used = false; 15169 } 15170 const availableImages = this.imageManager.listImages(); 15171 for (const layerId of this._order) { 15172 const layer = this._layers[layerId]; 15173 layer.recalculate(parameters, availableImages); 15174 if (!layer.isHidden(parameters.zoom) && layer.source) this.tileManagers[layer.source].used = true; 15175 } 15176 for (const id in managersUsedBefore) { 15177 const tileManager = this.tileManagers[id]; 15178 if (!!managersUsedBefore[id] !== !!tileManager.used) tileManager.fire(new MapSourceDataEvent("data", { 15179 sourceDataType: "visibility", 15180 sourceId: id 15181 })); 15182 } 15183 this.light.recalculate(parameters); 15184 this.sky.recalculate(parameters); 15185 this.projection.recalculate(parameters); 15186 this.z = parameters.zoom; 15187 if (changed) this.fire(new MapStyleDataEvent("data")); 15188 } 15189 _updateTilesForChangedImages() { 15190 const changedImages = Object.keys(this._changedImages); 15191 if (changedImages.length) { 15192 for (const name in this.tileManagers) this.tileManagers[name].reloadTilesForDependencies(["icons", "patterns"], changedImages); 15193 this._changedImages = {}; 15194 } 15195 } 15196 _updateTilesForChangedGlyphs() { 15197 if (this._glyphsDidChange) { 15198 for (const name in this.tileManagers) this.tileManagers[name].reloadTilesForDependencies(["glyphs"], [""]); 15199 this._glyphsDidChange = false; 15200 } 15201 } 15202 _updateWorkerLayers(updatedIds, removedIds) { 15203 this.dispatcher.broadcast("UL", { 15204 layers: this._serializeByIds(updatedIds, false), 15205 removedIds 15206 }); 15207 } 15208 _resetUpdates() { 15209 this._changed = false; 15210 this._updatedLayers = {}; 15211 this._removedLayers = {}; 15212 this._updatedSources = {}; 15213 this._updatedPaintProps = {}; 15214 this._changedImages = {}; 15215 this._glyphsDidChange = false; 15216 } 15217 /** 15218 * Update this style's state to match the given style JSON, performing only 15219 * the necessary mutations. 15220 * 15221 * May throw an Error ('Unimplemented: METHOD') if the maplibre-gl-style-spec 15222 * diff algorithm produces an operation that is not supported. 15223 * 15224 * @returns true if any changes were made; false otherwise 15225 */ 15226 setState(nextState, options = {}) { 15227 this._checkLoaded(); 15228 const serializedStyle = this.serialize(); 15229 nextState = options.transformStyle ? options.transformStyle(serializedStyle, nextState) : nextState; 15230 if ((options.validate ?? true) && validateStyleAndEmit(this, nextState)) return false; 15231 nextState = clone$2(nextState); 15232 nextState.layers = derefLayers(nextState.layers); 15233 const changes = diff(serializedStyle, nextState); 15234 const operations = this._getOperationsToPerform(changes); 15235 if (operations.unimplemented.length > 0) throw new Error(`Unimplemented: ${operations.unimplemented.join(", ")}.`); 15236 if (operations.operations.length === 0) return false; 15237 for (const styleChangeOperation of operations.operations) styleChangeOperation(); 15238 this.stylesheet = nextState; 15239 this._serializedLayers = null; 15240 this.fire(new MapStyleLoadEvent({ style: this })); 15241 return true; 15242 } 15243 /** 15244 * Translates a style diff into the calls that apply it to this style. 15245 * @param diff - the operations produced by the style-spec diff algorithm 15246 * @returns the operations to run, and the names of the commands that are not supported 15247 */ 15248 _getOperationsToPerform(diff) { 15249 const operations = []; 15250 const unimplemented = []; 15251 for (const op of diff) switch (op.command) { 15252 case "setCenter": 15253 case "setZoom": 15254 case "setBearing": 15255 case "setPitch": 15256 case "setRoll": continue; 15257 case "addLayer": { 15258 const [layer, before] = op.args; 15259 operations.push(() => this.addLayer(layer, before)); 15260 break; 15261 } 15262 case "removeLayer": { 15263 const [layerId] = op.args; 15264 operations.push(() => this.removeLayer(layerId)); 15265 break; 15266 } 15267 case "setPaintProperty": { 15268 const [layerId, name, value] = op.args; 15269 operations.push(() => this.setPaintProperty(layerId, name, value)); 15270 break; 15271 } 15272 case "setLayoutProperty": { 15273 const [layerId, name, value] = op.args; 15274 operations.push(() => this.setLayoutProperty(layerId, name, value)); 15275 break; 15276 } 15277 case "setFilter": { 15278 const [layerId, filter] = op.args; 15279 operations.push(() => this.setFilter(layerId, filter)); 15280 break; 15281 } 15282 case "addSource": { 15283 const [id, source] = op.args; 15284 operations.push(() => this.addSource(id, source)); 15285 break; 15286 } 15287 case "removeSource": { 15288 const [id] = op.args; 15289 operations.push(() => this.removeSource(id)); 15290 break; 15291 } 15292 case "setLayerZoomRange": { 15293 const [layerId, minzoom, maxzoom] = op.args; 15294 operations.push(() => this.setLayerZoomRange(layerId, minzoom, maxzoom)); 15295 break; 15296 } 15297 case "setLight": { 15298 const [light] = op.args; 15299 operations.push(() => this.setLight(light)); 15300 break; 15301 } 15302 case "setGeoJSONSourceData": { 15303 const [id, data] = op.args; 15304 operations.push(() => this.setGeoJSONSourceData(id, data)); 15305 break; 15306 } 15307 case "setGlyphs": { 15308 const [glyphsUrl] = op.args; 15309 operations.push(() => this.setGlyphs(glyphsUrl)); 15310 break; 15311 } 15312 case "setFontFaces": { 15313 const [fontFaces] = op.args; 15314 operations.push(() => this.setFontFaces(fontFaces)); 15315 break; 15316 } 15317 case "setSprite": { 15318 const [sprite] = op.args; 15319 operations.push(() => this.setSprite(sprite)); 15320 break; 15321 } 15322 case "setTerrain": { 15323 const [terrain] = op.args; 15324 operations.push(() => this.map.setTerrain(terrain)); 15325 break; 15326 } 15327 case "setSky": { 15328 const [sky] = op.args; 15329 operations.push(() => this.setSky(sky)); 15330 break; 15331 } 15332 case "setProjection": { 15333 const [projection] = op.args; 15334 operations.push(() => this.setProjection(projection)); 15335 break; 15336 } 15337 case "setGlobalState": { 15338 const [state] = op.args; 15339 operations.push(() => this.setGlobalState(state)); 15340 break; 15341 } 15342 case "setTransition": 15343 operations.push(() => {}); 15344 break; 15345 default: unimplemented.push(op.command); 15346 } 15347 return { 15348 operations, 15349 unimplemented 15350 }; 15351 } 15352 addImage(id, image) { 15353 if (this.getImage(id)) { 15354 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`An image named "${id}" already exists.`))); 15355 return; 15356 } 15357 this.imageManager.addImage(id, image); 15358 this._afterImageUpdated(id); 15359 } 15360 updateImage(id, image) { 15361 this.imageManager.updateImage(id, image); 15362 } 15363 getImage(id) { 15364 return this.imageManager.getImage(id); 15365 } 15366 setMissingImageResolver(resolver) { 15367 this.imageManager.setMissingImageResolver(resolver); 15368 } 15369 removeImage(id) { 15370 if (!this.getImage(id)) { 15371 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`An image named "${id}" does not exist.`))); 15372 return; 15373 } 15374 this.imageManager.removeImage(id); 15375 this._afterImageUpdated(id); 15376 } 15377 /** 15378 * Queues the tiles that depend on these images to be reloaded on the next update, which is 15379 * needed whenever an image appears, disappears or changes size. 15380 */ 15381 _markImagesChanged(ids) { 15382 for (const id of ids) this._changedImages[id] = true; 15383 } 15384 _afterImageUpdated(id) { 15385 this._changedImages[id] = true; 15386 this._imagesListDirty = true; 15387 this._changed = true; 15388 this.fire(new MapStyleDataEvent("data")); 15389 } 15390 listImages() { 15391 this._checkLoaded(); 15392 return this.imageManager.listImages(); 15393 } 15394 addSource(id, source, options = {}) { 15395 this._checkLoaded(); 15396 if (this.tileManagers[id] !== void 0) throw new Error(`Source "${id}" already exists.`); 15397 if (!source.type) throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(", ")}.`); 15398 if (SPEC_SOURCE_TYPES.has(source.type) && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return; 15399 if (this.map?._collectResourceTiming) source.collectResourceTiming = true; 15400 const tileManager = this.tileManagers[id] = new TileManager(id, source, this.dispatcher); 15401 tileManager.style = this; 15402 tileManager.setEventedParent(this, () => ({ 15403 isSourceLoaded: tileManager.loaded(), 15404 source: tileManager.serialize(), 15405 sourceId: id 15406 })); 15407 tileManager.onAdd(this.map); 15408 this._changed = true; 15409 } 15410 /** 15411 * Remove a source from this stylesheet, given its id. 15412 * @param id - id of the source to remove 15413 * @throws if no source is found with the given ID 15414 */ 15415 removeSource(id) { 15416 this._checkLoaded(); 15417 if (this.tileManagers[id] === void 0) throw new Error(`There is no source with this ID=${id}`); 15418 for (const layerId in this._layers) if (this._layers[layerId].source === id) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); 15419 const tileManager = this.tileManagers[id]; 15420 delete this.tileManagers[id]; 15421 delete this._updatedSources[id]; 15422 tileManager.fire(new MapSourceDataEvent("data", { 15423 sourceDataType: "metadata", 15424 sourceId: id 15425 })); 15426 tileManager.setEventedParent(null); 15427 tileManager.onRemove(this.map); 15428 this._changed = true; 15429 } 15430 /** 15431 * Set the data of a GeoJSON source, given its id. 15432 * @param id - id of the source 15433 * @param data - GeoJSON source 15434 */ 15435 setGeoJSONSourceData(id, data) { 15436 this._checkLoaded(); 15437 if (this.tileManagers[id] === void 0) throw new Error(`There is no source with this ID=${id}`); 15438 const geojsonSource = this.tileManagers[id].getSource(); 15439 if (geojsonSource.type !== "geojson") throw new Error(`geojsonSource.type is ${geojsonSource.type}, which is !== 'geojson`); 15440 geojsonSource.setData(data); 15441 this._changed = true; 15442 } 15443 /** 15444 * Get a source by ID. 15445 * @param id - ID of the desired source 15446 * @returns source 15447 */ 15448 getSource(id) { 15449 return this.tileManagers[id]?.getSource(); 15450 } 15451 /** 15452 * Add a layer to the map style. The layer will be inserted before the layer with 15453 * ID `before`, or appended if `before` is omitted. 15454 * @param layerObject - The style layer to add. 15455 * @param before - ID of an existing layer to insert before 15456 * @param options - Style setter options. 15457 */ 15458 addLayer(layerObject, before, options = {}) { 15459 this._checkLoaded(); 15460 const id = layerObject.id; 15461 if (this.getLayer(id)) { 15462 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Layer "${id}" already exists on this map.`))); 15463 return; 15464 } 15465 let layer; 15466 if (layerObject.type === "custom") { 15467 if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return; 15468 layer = createStyleLayer(layerObject, this._globalState); 15469 } else { 15470 if ("source" in layerObject && typeof layerObject.source === "object") { 15471 this.addSource(id, layerObject.source); 15472 layerObject = clone$2(layerObject); 15473 layerObject = extend(layerObject, { source: id }); 15474 } 15475 if (this._validate(validateStyle.layer, `layers.${id}`, layerObject, { arrayIndex: -1 }, options)) return; 15476 layer = createStyleLayer(layerObject, this._globalState); 15477 this._validateLayer(layer); 15478 layer.setEventedParent(this, { layer: { id } }); 15479 } 15480 const index = before ? this._order.indexOf(before) : this._order.length; 15481 if (before && index === -1) { 15482 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot add layer "${id}" before non-existing layer "${before}".`))); 15483 return; 15484 } 15485 this._order.splice(index, 0, id); 15486 this._layerOrderChanged = true; 15487 this._layers[id] = layer; 15488 if (this._removedLayers[id] && layer.source && layer.type !== "custom") { 15489 const removed = this._removedLayers[id]; 15490 delete this._removedLayers[id]; 15491 if (removed.type !== layer.type) this._updatedSources[layer.source] = "clear"; 15492 else { 15493 this._updatedSources[layer.source] = "reload"; 15494 this.tileManagers[layer.source].pause(); 15495 } 15496 } 15497 this._updateLayer(layer); 15498 if (layer.onAdd) layer.onAdd(this.map); 15499 } 15500 /** 15501 * Moves a layer to a different z-position. The layer will be inserted before the layer with 15502 * ID `before`, or appended if `before` is omitted. 15503 * @param id - ID of the layer to move 15504 * @param before - ID of an existing layer to insert before 15505 */ 15506 moveLayer(id, before) { 15507 this._checkLoaded(); 15508 this._changed = true; 15509 if (!this._layers[id]) { 15510 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`))); 15511 return; 15512 } 15513 if (id === before) return; 15514 if (before && !this._order.includes(before)) { 15515 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot move layer "${id}" before non-existing layer "${before}".`))); 15516 return; 15517 } 15518 const index = this._order.indexOf(id); 15519 this._order.splice(index, 1); 15520 const newIndex = before ? this._order.indexOf(before) : this._order.length; 15521 this._order.splice(newIndex, 0, id); 15522 this._layerOrderChanged = true; 15523 } 15524 /** 15525 * Remove the layer with the given id from the style. 15526 * A {@link ErrorEvent} event will be fired if no such layer exists. 15527 * 15528 * @param id - id of the layer to remove 15529 */ 15530 removeLayer(id) { 15531 this._checkLoaded(); 15532 const layer = this._layers[id]; 15533 if (!layer) { 15534 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot remove non-existing layer "${id}".`))); 15535 return; 15536 } 15537 layer.setEventedParent(null); 15538 const index = this._order.indexOf(id); 15539 this._order.splice(index, 1); 15540 this._layerOrderChanged = true; 15541 this._changed = true; 15542 this._removedLayers[id] = layer; 15543 delete this._layers[id]; 15544 if (this._serializedLayers) delete this._serializedLayers[id]; 15545 delete this._updatedLayers[id]; 15546 delete this._updatedPaintProps[id]; 15547 if (layer.type === "symbol" && layer.source) this.tileManagers[layer.source]?.resetMaxContentElevation(); 15548 if (layer.onRemove) layer.onRemove(this.map); 15549 } 15550 /** 15551 * Return the style layer object with the given `id`. 15552 * 15553 * @param id - id of the desired layer 15554 * @returns a layer, if one with the given `id` exists 15555 */ 15556 getLayer(id) { 15557 return this._layers[id]; 15558 } 15559 /** 15560 * Return the ids of all layers currently in the style, including custom layers, in order. 15561 * 15562 * @returns ids of layers, in order 15563 */ 15564 getLayersOrder() { 15565 return [...this._order]; 15566 } 15567 /** 15568 * Checks if a specific layer is present within the style. 15569 * 15570 * @param id - the id of the desired layer 15571 * @returns a boolean specifying if the given layer is present 15572 */ 15573 hasLayer(id) { 15574 return id in this._layers; 15575 } 15576 setLayerZoomRange(layerId, minzoom, maxzoom) { 15577 this._checkLoaded(); 15578 const layer = this.getLayer(layerId); 15579 if (!layer) { 15580 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot set the zoom range of non-existing layer "${layerId}".`))); 15581 return; 15582 } 15583 if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; 15584 if (minzoom != null) layer.minzoom = minzoom; 15585 if (maxzoom != null) layer.maxzoom = maxzoom; 15586 this._updateLayer(layer); 15587 } 15588 setFilter(layerId, filter, options = {}) { 15589 this._checkLoaded(); 15590 const layer = this.getLayer(layerId); 15591 if (!layer) { 15592 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot filter non-existing layer "${layerId}".`))); 15593 return; 15594 } 15595 if (deepEqual(layer.filter, filter)) return; 15596 if (filter === null || filter === void 0) { 15597 layer.setFilter(void 0); 15598 this._updateLayer(layer); 15599 return; 15600 } 15601 if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, null, options)) return; 15602 layer.setFilter(clone$2(filter)); 15603 this._updateLayer(layer); 15604 } 15605 /** 15606 * Get a layer's filter object 15607 * @param layer - the layer to inspect 15608 * @returns the layer's filter, if any 15609 */ 15610 getFilter(layer) { 15611 return clone$2(this.getLayer(layer).filter); 15612 } 15613 setLayoutProperty(layerId, name, value, options = {}) { 15614 this._checkLoaded(); 15615 const layer = this.getLayer(layerId); 15616 if (!layer) { 15617 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot style non-existing layer "${layerId}".`))); 15618 return; 15619 } 15620 if (deepEqual(layer.getLayoutProperty(name), value)) return; 15621 layer.setLayoutProperty(name, value, options); 15622 this._updateLayer(layer); 15623 } 15624 /** 15625 * Get a layout property's value from a given layer 15626 * @param layerId - the layer to inspect 15627 * @param name - the name of the layout property 15628 * @returns the property value 15629 */ 15630 getLayoutProperty(layerId, name) { 15631 const layer = this.getLayer(layerId); 15632 if (!layer) { 15633 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot get style of non-existing layer "${layerId}".`))); 15634 return; 15635 } 15636 return layer.getLayoutProperty(name); 15637 } 15638 setPaintProperty(layerId, name, value, options = {}) { 15639 this._checkLoaded(); 15640 const layer = this.getLayer(layerId); 15641 if (!layer) { 15642 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Cannot style non-existing layer "${layerId}".`))); 15643 return; 15644 } 15645 if (deepEqual(layer.getPaintProperty(name), value)) return; 15646 this._updatePaintProperty(layer, name, value, options); 15647 } 15648 _updatePaintProperty(layer, name, value, options = {}) { 15649 if (layer.setPaintProperty(name, value, options)) this._updateLayer(layer); 15650 if (isRasterStyleLayer(layer) && name === "raster-fade-duration") this.tileManagers[layer.source].setRasterFadeDuration(value); 15651 this._changed = true; 15652 this._updatedPaintProps[layer.id] = true; 15653 if (layer.type === "symbol") this.triggerSymbolPlacement(); 15654 this._serializedLayers = null; 15655 } 15656 getPaintProperty(layer, name) { 15657 return this.getLayer(layer).getPaintProperty(name); 15658 } 15659 setFeatureState(target, state) { 15660 this._checkLoaded(); 15661 const sourceId = target.source; 15662 const sourceLayer = target.sourceLayer; 15663 const tileManager = this.tileManagers[sourceId]; 15664 if (tileManager === void 0) { 15665 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The source '${sourceId}' does not exist in the map's style.`))); 15666 return; 15667 } 15668 const sourceType = tileManager.getSource().type; 15669 if (sourceType === "geojson" && sourceLayer) { 15670 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("GeoJSON sources cannot have a sourceLayer parameter."))); 15671 return; 15672 } 15673 if (sourceType === "vector" && !sourceLayer) { 15674 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The sourceLayer parameter must be provided for vector source types."))); 15675 return; 15676 } 15677 if (target.id === void 0) { 15678 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The feature id parameter must be provided."))); 15679 return; 15680 } 15681 const forbiddenStateKeys = [ 15682 "__proto__", 15683 "constructor", 15684 "prototype" 15685 ]; 15686 if (state && Object.keys(state).some((stateKey) => forbiddenStateKeys.includes(stateKey))) { 15687 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The feature state should not include one of the following keys: ${forbiddenStateKeys}`))); 15688 return; 15689 } 15690 tileManager.setFeatureState(sourceLayer, target.id, state); 15691 } 15692 removeFeatureState(target, key) { 15693 this._checkLoaded(); 15694 const sourceId = target.source; 15695 const tileManager = this.tileManagers[sourceId]; 15696 if (tileManager === void 0) { 15697 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The source '${sourceId}' does not exist in the map's style.`))); 15698 return; 15699 } 15700 const sourceType = tileManager.getSource().type; 15701 const sourceLayer = sourceType === "vector" ? target.sourceLayer : void 0; 15702 if (sourceType === "vector" && !sourceLayer) { 15703 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The sourceLayer parameter must be provided for vector source types."))); 15704 return; 15705 } 15706 if (key && typeof target.id !== "string" && typeof target.id !== "number") { 15707 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("A feature id is required to remove its specific state property."))); 15708 return; 15709 } 15710 tileManager.removeFeatureState(sourceLayer, target.id, key); 15711 } 15712 getFeatureState(target) { 15713 this._checkLoaded(); 15714 const sourceId = target.source; 15715 const sourceLayer = target.sourceLayer; 15716 const tileManager = this.tileManagers[sourceId]; 15717 if (tileManager === void 0) { 15718 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The source '${sourceId}' does not exist in the map's style.`))); 15719 return; 15720 } 15721 if (tileManager.getSource().type === "vector" && !sourceLayer) { 15722 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The sourceLayer parameter must be provided for vector source types."))); 15723 return; 15724 } 15725 if (target.id === void 0) this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The feature id parameter must be provided."))); 15726 return tileManager.getFeatureState(sourceLayer, target.id); 15727 } 15728 getTransition() { 15729 return extend({ 15730 duration: 300, 15731 delay: 0 15732 }, this.stylesheet?.transition); 15733 } 15734 serialize() { 15735 if (!this._loaded) return; 15736 const sources = mapObject(this.tileManagers, (source) => source.serialize()); 15737 const layers = this._serializeByIds(this._order, true); 15738 const terrain = this.map.getTerrain() || void 0; 15739 const myStyleSheet = this.stylesheet; 15740 return filterObject({ 15741 version: myStyleSheet.version, 15742 name: myStyleSheet.name, 15743 metadata: myStyleSheet.metadata, 15744 light: myStyleSheet.light, 15745 sky: myStyleSheet.sky, 15746 center: myStyleSheet.center, 15747 zoom: myStyleSheet.zoom, 15748 bearing: myStyleSheet.bearing, 15749 pitch: myStyleSheet.pitch, 15750 sprite: myStyleSheet.sprite, 15751 glyphs: myStyleSheet.glyphs, 15752 "font-faces": myStyleSheet["font-faces"], 15753 transition: myStyleSheet.transition, 15754 projection: myStyleSheet.projection, 15755 state: myStyleSheet.state, 15756 sources, 15757 layers, 15758 terrain 15759 }, (value) => value !== void 0); 15760 } 15761 _updateLayer(layer) { 15762 this._updatedLayers[layer.id] = true; 15763 if (layer.source && !this._updatedSources[layer.source] && this.tileManagers[layer.source].getSource().type !== "raster") { 15764 this._updatedSources[layer.source] = "reload"; 15765 this.tileManagers[layer.source].pause(); 15766 } 15767 this._serializedLayers = null; 15768 this._changed = true; 15769 } 15770 _flattenAndSortRenderedFeatures(sourceResults) { 15771 const isLayer3D = (layerId) => this._layers[layerId].type === "fill-extrusion"; 15772 const layerIndex = {}; 15773 const features3D = []; 15774 for (let l = this._order.length - 1; l >= 0; l--) { 15775 const layerId = this._order[l]; 15776 if (isLayer3D(layerId)) { 15777 layerIndex[layerId] = l; 15778 for (const sourceResult of sourceResults) { 15779 const layerFeatures = sourceResult[layerId]; 15780 if (layerFeatures) for (const featureWrapper of layerFeatures) features3D.push(featureWrapper); 15781 } 15782 } 15783 } 15784 features3D.sort((a, b) => { 15785 return b.intersectionZ - a.intersectionZ; 15786 }); 15787 const features = []; 15788 for (let l = this._order.length - 1; l >= 0; l--) { 15789 const layerId = this._order[l]; 15790 if (isLayer3D(layerId)) for (let i = features3D.length - 1; i >= 0; i--) { 15791 const topmost3D = features3D[i].feature; 15792 if (layerIndex[topmost3D.layer.id] < l) break; 15793 features.push(topmost3D); 15794 features3D.pop(); 15795 } 15796 else for (const sourceResult of sourceResults) { 15797 const layerFeatures = sourceResult[layerId]; 15798 if (layerFeatures) for (const featureWrapper of layerFeatures) features.push(featureWrapper.feature); 15799 } 15800 } 15801 return features; 15802 } 15803 queryRenderedFeatures(queryGeometry, params, transform) { 15804 if (params?.filter) this._validate(validateStyle.filter, "queryRenderedFeatures.filter", params.filter, null, params); 15805 const includedSources = {}; 15806 if (params?.layers) { 15807 if (!(Array.isArray(params.layers) || params.layers instanceof Set)) { 15808 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("parameters.layers must be an Array or a Set of strings"))); 15809 return []; 15810 } 15811 for (const layerId of params.layers) { 15812 const layer = this._layers[layerId]; 15813 if (!layer) { 15814 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`))); 15815 return []; 15816 } 15817 includedSources[layer.source] = true; 15818 } 15819 } 15820 const sourceResults = []; 15821 params.availableImages = this.imageManager.listImages(); 15822 const serializedLayers = this._serializedAllLayers(); 15823 const layersAsSet = params.layers instanceof Set ? params.layers : Array.isArray(params.layers) ? new Set(params.layers) : null; 15824 const paramsStrict = { 15825 ...params, 15826 layers: layersAsSet, 15827 globalState: this._globalState 15828 }; 15829 for (const id in this.tileManagers) { 15830 if (params.layers && !includedSources[id]) continue; 15831 sourceResults.push(queryRenderedFeatures(this.tileManagers[id], this._layers, serializedLayers, queryGeometry, paramsStrict, transform, this.map.terrain ? (id, x, y) => this.map.terrain.getElevation(id, x, y) : void 0)); 15832 } 15833 if (this.placement) sourceResults.push(queryRenderedSymbols(this._layers, serializedLayers, this.tileManagers, queryGeometry, paramsStrict, this.placement.collisionIndex, this.placement.retainedQueryData)); 15834 return this._flattenAndSortRenderedFeatures(sourceResults); 15835 } 15836 querySourceFeatures(sourceID, params) { 15837 if (params?.filter) this._validate(validateStyle.filter, "querySourceFeatures.filter", params.filter, null, params); 15838 const tileManager = this.tileManagers[sourceID]; 15839 return tileManager ? querySourceFeatures(tileManager, params ? { 15840 ...params, 15841 globalState: this._globalState 15842 } : { globalState: this._globalState }) : []; 15843 } 15844 getLight() { 15845 return this.light.getLight(); 15846 } 15847 setLight(lightOptions, options = {}) { 15848 this._checkLoaded(); 15849 const light = this.light.getLight(); 15850 let _update = false; 15851 for (const key in lightOptions) if (!deepEqual(lightOptions[key], light[key])) { 15852 _update = true; 15853 break; 15854 } 15855 if (!_update) return; 15856 const parameters = { 15857 now: now(), 15858 transition: extend({ 15859 duration: 300, 15860 delay: 0 15861 }, this.stylesheet.transition) 15862 }; 15863 this.light.setLight(lightOptions, options); 15864 this.light.updateTransitions(parameters); 15865 } 15866 getProjection() { 15867 return this.stylesheet?.projection; 15868 } 15869 setProjection(projection) { 15870 this._checkLoaded(); 15871 const resolvedProjection = projection ?? { type: "mercator" }; 15872 this.stylesheet.projection = projection; 15873 if (this.projection) { 15874 if (this.projection.name === resolvedProjection.type) return; 15875 this.projection.destroy(); 15876 delete this.projection; 15877 } 15878 this._setProjectionInternal(resolvedProjection.type); 15879 } 15880 getSky() { 15881 return this.stylesheet?.sky; 15882 } 15883 setSky(skyOptions, options = {}) { 15884 this._checkLoaded(); 15885 const sky = this.getSky(); 15886 let update = false; 15887 if (!skyOptions && !sky) return; 15888 if (skyOptions && !sky) update = true; 15889 else if (!skyOptions && sky) update = true; 15890 else for (const key in skyOptions) if (!deepEqual(skyOptions[key], sky[key])) { 15891 update = true; 15892 break; 15893 } 15894 if (!update) return; 15895 const parameters = { 15896 now: now(), 15897 transition: extend({ 15898 duration: 300, 15899 delay: 0 15900 }, this.stylesheet.transition) 15901 }; 15902 if (!this.sky.setSky(skyOptions, options)) return; 15903 this.stylesheet.sky = skyOptions; 15904 this.sky.updateTransitions(parameters); 15905 } 15906 _setProjectionInternal(name) { 15907 const projectionObjects = createProjectionFromName(name, this.map._camera?.transform.constrainOverride, this._globalState); 15908 this.projection = projectionObjects.projection; 15909 this.map.migrateProjection(projectionObjects.transform, projectionObjects.cameraHelper); 15910 for (const key in this.tileManagers) this.tileManagers[key].reload(); 15911 } 15912 _validate(validate, key, value, props, options = {}) { 15913 if (options.validate === false) return false; 15914 return validateAndEmit(this, validate, { 15915 key, 15916 style: this.serialize(), 15917 value, 15918 ...props 15919 }, options); 15920 } 15921 _remove(mapRemoved = true) { 15922 if (this._frameRequest) { 15923 this._frameRequest.abort(); 15924 this._frameRequest = null; 15925 } 15926 if (this._loadStyleRequest) { 15927 this._loadStyleRequest.abort(); 15928 this._loadStyleRequest = null; 15929 } 15930 if (this._spriteRequest) { 15931 this._spriteRequest.abort(); 15932 this._spriteRequest = null; 15933 } 15934 rtlMainThreadPluginFactory().off(RTLPluginLoadedEventName, this._rtlPluginLoaded); 15935 for (const layerId in this._layers) this._layers[layerId].setEventedParent(null); 15936 for (const id in this.tileManagers) { 15937 const tileManager = this.tileManagers[id]; 15938 tileManager.setEventedParent(null); 15939 tileManager.onRemove(this.map); 15940 } 15941 this.imageManager.setEventedParent(null); 15942 this.setEventedParent(null); 15943 if (mapRemoved) this.dispatcher.broadcast("RM", void 0); 15944 this.dispatcher.remove(mapRemoved); 15945 } 15946 _clearSource(id) { 15947 this.tileManagers[id].clearTiles(); 15948 } 15949 _reloadSource(id) { 15950 this.tileManagers[id].resume(); 15951 this.tileManagers[id].reload(); 15952 } 15953 _updateSources(transform) { 15954 for (const id in this.tileManagers) this.tileManagers[id].update(transform, this.map.terrain); 15955 } 15956 _generateCollisionBoxes() { 15957 for (const id in this.tileManagers) this._reloadSource(id); 15958 } 15959 /** 15960 * Re-places symbols on the next frame. 15961 * Placement is skipped while its inputs look unchanged, so call this when something 15962 * the map cannot see for itself has moved symbols. 15963 */ 15964 triggerSymbolPlacement() { 15965 this._symbolPlacementTriggered = true; 15966 } 15967 /** 15968 * Whether re-running symbol placement could produce a different result than the last run. 15969 * Every camera and viewport parameter placement can observe perturbs the model-view-projection 15970 * matrix, so comparing the matrix covers them all. Inputs the matrix cannot see arrive as 15971 * {@link triggerSymbolPlacement} calls. 15972 * 15973 * @internal 15974 */ 15975 _placementInputsChanged(transform, showCollisionBoxes, crossSourceCollisions) { 15976 const lastPlacement = this.pauseablePlacement; 15977 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); 15978 } 15979 _updatePlacement(transform, showCollisionBoxes, fadeDuration, crossSourceCollisions, forceFullPlacement = false) { 15980 let symbolBucketsChanged = false; 15981 let placementCommitted = false; 15982 const layerTiles = {}; 15983 for (const layerID of this._order) { 15984 const styleLayer = this._layers[layerID]; 15985 if (styleLayer.type !== "symbol") continue; 15986 if (!layerTiles[styleLayer.source]) { 15987 const tileManager = this.tileManagers[styleLayer.source]; 15988 layerTiles[styleLayer.source] = tileManager.getRenderableIds(true).map((id) => tileManager.getTileByID(id)).sort((a, b) => b.tileID.overscaledZ - a.tileID.overscaledZ || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); 15989 } 15990 const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng); 15991 symbolBucketsChanged ||= layerBucketsChanged; 15992 } 15993 this.crossTileSymbolIndex.pruneUnusedLayers(this._order); 15994 forceFullPlacement ||= this._layerOrderChanged || fadeDuration === 0; 15995 const placementInputsChanged = symbolBucketsChanged || this._placementInputsChanged(transform, showCollisionBoxes, crossSourceCollisions); 15996 const placementSettled = this.pauseablePlacement?.isDone() && !this.placement.stillRecent(now(), transform.zoom); 15997 if (forceFullPlacement || !this.pauseablePlacement || placementSettled && (placementInputsChanged || this.placement.stale)) { 15998 this._symbolPlacementTriggered = false; 15999 this._placedProjectionTransition = this.projection?.transitionState; 16000 this.pauseablePlacement = new PauseablePlacement(transform, this.map.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement); 16001 this._layerOrderChanged = false; 16002 } 16003 if (this.pauseablePlacement.isDone()) { 16004 if (placementInputsChanged) this.placement.setStale(); 16005 } else { 16006 this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); 16007 if (this.pauseablePlacement.isDone()) { 16008 this.placement = this.pauseablePlacement.commit(now()); 16009 placementCommitted = true; 16010 } 16011 if (symbolBucketsChanged) this.pauseablePlacement.placement.setStale(); 16012 } 16013 if (placementCommitted || symbolBucketsChanged) for (const layerID of this._order) { 16014 const styleLayer = this._layers[layerID]; 16015 if (styleLayer.type !== "symbol") continue; 16016 this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); 16017 } 16018 return !this.pauseablePlacement.isDone() || this.placement.hasTransitions(now()); 16019 } 16020 _releaseSymbolFadeTiles() { 16021 for (const id in this.tileManagers) this.tileManagers[id].releaseSymbolFadeTiles(); 16022 } 16023 async getImages(mapId, params) { 16024 const images = await this.imageManager.getImages(params.icons); 16025 this._updateTilesForChangedImages(); 16026 const tileManager = this.tileManagers[params.source]; 16027 if (tileManager) tileManager.setDependencies(params.tileID.key, params.type, params.icons); 16028 return images; 16029 } 16030 async getGlyphs(mapId, params) { 16031 const glyphs = await this.glyphManager.getGlyphs(params.stacks); 16032 const tileManager = this.tileManagers[params.source]; 16033 if (tileManager) tileManager.setDependencies(params.tileID.key, params.type, [""]); 16034 return glyphs; 16035 } 16036 getGlyphsUrl() { 16037 return this.stylesheet.glyphs || null; 16038 } 16039 setGlyphs(glyphsUrl, options = {}) { 16040 this._checkLoaded(); 16041 if (glyphsUrl && this._validate(validateStyle.glyphs, "glyphs", glyphsUrl, null, options)) return; 16042 this._changed = true; 16043 this._glyphsDidChange = true; 16044 this.stylesheet.glyphs = glyphsUrl; 16045 this.glyphManager.entries = {}; 16046 this.glyphManager.setURL(glyphsUrl); 16047 } 16048 getFontFaces() { 16049 return this.stylesheet["font-faces"] || null; 16050 } 16051 setFontFaces(fontFaces) { 16052 this._checkLoaded(); 16053 this._changed = true; 16054 this._glyphsDidChange = true; 16055 this.stylesheet["font-faces"] = fontFaces; 16056 this.glyphManager.setFontFaces(fontFaces); 16057 } 16058 async getDashes(mapId, params) { 16059 const result = {}; 16060 for (const [key, dash] of Object.entries(params.dashes)) result[key] = this.lineAtlas.getDash(dash.dasharray, dash.round); 16061 return result; 16062 } 16063 /** 16064 * Add a sprite. 16065 * 16066 * @param id - The id of the desired sprite 16067 * @param url - The url to load the desired sprite from 16068 * @param options - The style setter options 16069 * @param completion - The completion handler 16070 */ 16071 addSprite(id, url, options = {}, completion) { 16072 this._checkLoaded(); 16073 const spriteToAdd = [{ 16074 id, 16075 url 16076 }]; 16077 const updatedSprite = [...coerceSpriteToArray(this.stylesheet.sprite), ...spriteToAdd]; 16078 if (this._validate(validateStyle.sprite, "sprite", updatedSprite, null, options)) return; 16079 this.stylesheet.sprite = updatedSprite; 16080 this._loadSprite(spriteToAdd, true, completion); 16081 } 16082 /** 16083 * Remove a sprite by its id. When the last sprite is removed, the whole `this.stylesheet.sprite` object becomes 16084 * `undefined`. This falsy `undefined` value later prevents attempts to load the sprite when it's absent. 16085 * 16086 * @param id - the id of the sprite to remove 16087 */ 16088 removeSprite(id) { 16089 this._checkLoaded(); 16090 const internalSpriteRepresentation = coerceSpriteToArray(this.stylesheet.sprite); 16091 if (!internalSpriteRepresentation.find((sprite) => sprite.id === id)) { 16092 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Sprite "${id}" doesn't exists on this map.`))); 16093 return; 16094 } 16095 const changedImages = this.imageManager.removeSpriteImages(id); 16096 this._markImagesChanged(changedImages); 16097 internalSpriteRepresentation.splice(internalSpriteRepresentation.findIndex((sprite) => sprite.id === id), 1); 16098 this.stylesheet.sprite = internalSpriteRepresentation.length > 0 ? internalSpriteRepresentation : void 0; 16099 this._imagesListDirty = true; 16100 this._changed = true; 16101 this.fire(new MapStyleDataEvent("data")); 16102 } 16103 /** 16104 * Get the current sprite value. 16105 * 16106 * @returns empty array when no sprite is set; id-url pairs otherwise 16107 */ 16108 getSprite() { 16109 return coerceSpriteToArray(this.stylesheet.sprite); 16110 } 16111 /** 16112 * Set a new value for the style's sprite. 16113 * 16114 * @param sprite - new sprite value 16115 * @param options - style setter options 16116 * @param completion - the completion handler 16117 */ 16118 setSprite(sprite, options = {}, completion) { 16119 this._checkLoaded(); 16120 if (sprite && this._validate(validateStyle.sprite, "sprite", sprite, null, options)) return; 16121 this.stylesheet.sprite = sprite; 16122 if (sprite) this._loadSprite(sprite, true, completion); 16123 else { 16124 this._unloadSprite(); 16125 if (completion) completion(null); 16126 } 16127 } 16128 /** 16129 * Destroys all internal resources of the style (sources, images, layers, etc.) 16130 */ 16131 destroy() { 16132 if (this._frameRequest) { 16133 this._frameRequest.abort(); 16134 this._frameRequest = null; 16135 } 16136 if (this._loadStyleRequest) { 16137 this._loadStyleRequest.abort(); 16138 this._loadStyleRequest = null; 16139 } 16140 if (this._spriteRequest) { 16141 this._spriteRequest.abort(); 16142 this._spriteRequest = null; 16143 } 16144 for (const id in this.tileManagers) { 16145 const tileManager = this.tileManagers[id]; 16146 tileManager.setEventedParent(null); 16147 tileManager.onRemove(this.map); 16148 } 16149 this.tileManagers = {}; 16150 if (this.imageManager) { 16151 this.imageManager.setEventedParent(null); 16152 this.imageManager.destroy(); 16153 this.patternAtlas.destroy(); 16154 } 16155 if (this.glyphManager) this.glyphManager.destroy(); 16156 for (const layerId in this._layers) { 16157 const layer = this._layers[layerId]; 16158 layer.setEventedParent(null); 16159 if (layer.onRemove) layer.onRemove(this.map); 16160 } 16161 this._setInitialValues(); 16162 this.setEventedParent(null); 16163 this.dispatcher.unregisterMessageHandler("GG"); 16164 this.dispatcher.unregisterMessageHandler("GI"); 16165 this.dispatcher.unregisterMessageHandler("GDA"); 16166 this.dispatcher.remove(true); 16167 this._listeners = {}; 16168 this._oneTimeListeners = {}; 16169 } 16170}; 16171//#endregion 16172//#region src/data/raster_bounds_attributes.ts 16173const rasterBoundsAttributes = createLayout([{ 16174 name: "a_pos", 16175 type: "Int16", 16176 components: 2 16177}, { 16178 name: "a_texture_pos", 16179 type: "Int16", 16180 components: 2 16181}]); 16182//#endregion 16183//#region src/webgl/vertex_array_object.ts 16184/** 16185* @internal 16186* A vertex array object used to pass data to the webgl code 16187*/ 16188var VertexArrayObject = class { 16189 constructor() { 16190 this.boundProgram = null; 16191 this.boundLayoutVertexBuffer = null; 16192 this.boundPaintVertexBuffers = []; 16193 this.boundIndexBuffer = null; 16194 this.boundVertexOffset = null; 16195 this.boundDynamicVertexBuffer = null; 16196 this.vao = null; 16197 } 16198 bind(context, program, layoutVertexBuffer, paintVertexBuffers, indexBuffer, vertexOffset, dynamicVertexBuffer, dynamicVertexBuffer2, dynamicVertexBuffer3) { 16199 this.context = context; 16200 let paintBuffersDiffer = this.boundPaintVertexBuffers.length !== paintVertexBuffers.length; 16201 for (let i = 0; !paintBuffersDiffer && i < paintVertexBuffers.length; i++) if (this.boundPaintVertexBuffers[i] !== paintVertexBuffers[i]) paintBuffersDiffer = true; 16202 if (!this.vao || this.boundProgram !== program || this.boundLayoutVertexBuffer !== layoutVertexBuffer || paintBuffersDiffer || this.boundIndexBuffer !== indexBuffer || this.boundVertexOffset !== vertexOffset || this.boundDynamicVertexBuffer !== dynamicVertexBuffer || this.boundDynamicVertexBuffer2 !== dynamicVertexBuffer2 || this.boundDynamicVertexBuffer3 !== dynamicVertexBuffer3) this.freshBind(program, layoutVertexBuffer, paintVertexBuffers, indexBuffer, vertexOffset, dynamicVertexBuffer, dynamicVertexBuffer2, dynamicVertexBuffer3); 16203 else context.bindVertexArray.set(this.vao); 16204 } 16205 freshBind(program, layoutVertexBuffer, paintVertexBuffers, indexBuffer, vertexOffset, dynamicVertexBuffer, dynamicVertexBuffer2, dynamicVertexBuffer3) { 16206 const numNextAttributes = program.numAttributes; 16207 const context = this.context; 16208 const gl = context.gl; 16209 if (this.vao) this.destroy(); 16210 this.vao = context.createVertexArray(); 16211 context.bindVertexArray.set(this.vao); 16212 this.boundProgram = program; 16213 this.boundLayoutVertexBuffer = layoutVertexBuffer; 16214 this.boundPaintVertexBuffers = paintVertexBuffers; 16215 this.boundIndexBuffer = indexBuffer; 16216 this.boundVertexOffset = vertexOffset; 16217 this.boundDynamicVertexBuffer = dynamicVertexBuffer; 16218 this.boundDynamicVertexBuffer2 = dynamicVertexBuffer2; 16219 this.boundDynamicVertexBuffer3 = dynamicVertexBuffer3; 16220 layoutVertexBuffer.enableAttributes(gl, program); 16221 for (const vertexBuffer of paintVertexBuffers) vertexBuffer.enableAttributes(gl, program); 16222 if (dynamicVertexBuffer) dynamicVertexBuffer.enableAttributes(gl, program); 16223 if (dynamicVertexBuffer2) dynamicVertexBuffer2.enableAttributes(gl, program); 16224 if (dynamicVertexBuffer3) dynamicVertexBuffer3.enableAttributes(gl, program); 16225 layoutVertexBuffer.bind(); 16226 layoutVertexBuffer.setVertexAttribPointers(gl, program, vertexOffset); 16227 for (const vertexBuffer of paintVertexBuffers) { 16228 vertexBuffer.bind(); 16229 vertexBuffer.setVertexAttribPointers(gl, program, vertexOffset); 16230 } 16231 if (dynamicVertexBuffer) { 16232 dynamicVertexBuffer.bind(); 16233 dynamicVertexBuffer.setVertexAttribPointers(gl, program, vertexOffset); 16234 } 16235 if (indexBuffer) indexBuffer.bind(); 16236 if (dynamicVertexBuffer2) { 16237 dynamicVertexBuffer2.bind(); 16238 dynamicVertexBuffer2.setVertexAttribPointers(gl, program, vertexOffset); 16239 } 16240 if (dynamicVertexBuffer3) { 16241 dynamicVertexBuffer3.bind(); 16242 dynamicVertexBuffer3.setVertexAttribPointers(gl, program, vertexOffset); 16243 } 16244 context.currentNumAttributes = numNextAttributes; 16245 } 16246 destroy() { 16247 if (this.vao) { 16248 this.context.deleteVertexArray(this.vao); 16249 this.vao = null; 16250 } 16251 } 16252}; 16253//#endregion 16254//#region src/webgl/program/terrain_program.ts 16255const terrainPreludeUniforms = (context, locations) => ({ 16256 "u_depth": new Uniform1i(context, locations.u_depth), 16257 "u_terrain": new Uniform1i(context, locations.u_terrain) 16258}); 16259const terrainUniforms = (context, locations) => ({ 16260 "u_texture": new Uniform1i(context, locations.u_texture), 16261 "u_ele_delta": new Uniform1f(context, locations.u_ele_delta), 16262 "u_fog_matrix": new UniformMatrix4f(context, locations.u_fog_matrix), 16263 "u_fog_color": new UniformColor(context, locations.u_fog_color), 16264 "u_fog_ground_blend": new Uniform1f(context, locations.u_fog_ground_blend), 16265 "u_fog_ground_blend_opacity": new Uniform1f(context, locations.u_fog_ground_blend_opacity), 16266 "u_horizon_color": new UniformColor(context, locations.u_horizon_color), 16267 "u_horizon_fog_blend": new Uniform1f(context, locations.u_horizon_fog_blend), 16268 "u_is_globe_mode": new Uniform1f(context, locations.u_is_globe_mode) 16269}); 16270const terrainDepthUniforms = (context, locations) => ({ "u_ele_delta": new Uniform1f(context, locations.u_ele_delta) }); 16271const terrainUniformValues = (eleDelta, fogMatrix, sky, pitch, isGlobeMode) => ({ 16272 "u_texture": 0, 16273 "u_ele_delta": eleDelta, 16274 "u_fog_matrix": fogMatrix, 16275 "u_fog_color": sky ? sky.properties.get("fog-color") : Color.white, 16276 "u_fog_ground_blend": sky ? sky.properties.get("fog-ground-blend") : 1, 16277 "u_fog_ground_blend_opacity": isGlobeMode ? 0 : sky ? sky.calculateFogBlendOpacity(pitch) : 0, 16278 "u_horizon_color": sky ? sky.properties.get("horizon-color") : Color.white, 16279 "u_horizon_fog_blend": sky ? sky.properties.get("horizon-fog-blend") : 1, 16280 "u_is_globe_mode": isGlobeMode ? 1 : 0 16281}); 16282const terrainDepthUniformValues = (eleDelta) => ({ "u_ele_delta": eleDelta }); 16283//#endregion 16284//#region src/webgl/uniform_buffer.ts 16285const UBO_BINDINGS = { 16286 ProjectionUBO: 0, 16287 TerrainUBO: 1, 16288 FrameUBO: 2 16289}; 16290function applyUBOBindings(gl, program) { 16291 for (const [name, binding] of Object.entries(UBO_BINDINGS)) { 16292 const index = gl.getUniformBlockIndex(program, name); 16293 if (index !== gl.INVALID_INDEX) gl.uniformBlockBinding(program, index, binding); 16294 } 16295} 16296const STD140_SIZE_AND_ALIGNMENT_WORDS = { 16297 float: [1, 1], 16298 int: [1, 1], 16299 vec2: [2, 2], 16300 vec4: [4, 4], 16301 mat4: [16, 4] 16302}; 16303function std140Layout(members) { 16304 const offsets = {}; 16305 let words = 0; 16306 for (const { name, type } of members) { 16307 const [size, alignment] = STD140_SIZE_AND_ALIGNMENT_WORDS[type]; 16308 words = Math.ceil(words / alignment) * alignment; 16309 offsets[name] = words; 16310 words += size; 16311 } 16312 return { 16313 offsets, 16314 contentWords: words, 16315 sizeWords: Math.ceil(words / 4) * 4 16316 }; 16317} 16318/** 16319* @internal 16320* The buffer behind one std140 uniform block, bound to a fixed binding point. Callers write members into 16321* `pending` at the layout's offsets and call `upload`, which skips the GPU write when nothing changed. Uploads 16322* use `bufferData` because Apple's OpenGL driver stalls a `bufferSubData` into a buffer a pending draw still reads (#8468). 16323*/ 16324var UniformBuffer = class { 16325 constructor(context, binding, layout) { 16326 this.context = context; 16327 this.binding = binding; 16328 this.contentWords = layout.contentWords; 16329 const gl = context.gl; 16330 this.buffer = gl.createBuffer(); 16331 gl.bindBuffer(gl.UNIFORM_BUFFER, this.buffer); 16332 gl.bufferData(gl.UNIFORM_BUFFER, layout.sizeWords * 4, gl.DYNAMIC_DRAW); 16333 gl.bindBufferBase(gl.UNIFORM_BUFFER, binding, this.buffer); 16334 this.uploaded = new Float32Array(layout.sizeWords); 16335 this.pending = new Float32Array(layout.sizeWords); 16336 this.uploadedWords = new Uint32Array(this.uploaded.buffer); 16337 this.pendingWords = new Uint32Array(this.pending.buffer); 16338 this.hasData = false; 16339 this.bindingDirty = false; 16340 } 16341 upload() { 16342 const gl = this.context.gl; 16343 let changed = !this.hasData; 16344 if (!changed) { 16345 const words = this.pendingWords; 16346 const uploadedWords = this.uploadedWords; 16347 for (let i = 0; i < this.contentWords; i++) if (words[i] !== uploadedWords[i]) { 16348 changed = true; 16349 break; 16350 } 16351 } 16352 this.bind(); 16353 if (!changed) return; 16354 gl.bindBufferBase(gl.UNIFORM_BUFFER, this.binding, this.buffer); 16355 gl.bufferData(gl.UNIFORM_BUFFER, this.pending, gl.DYNAMIC_DRAW); 16356 this.uploaded.set(this.pending); 16357 this.hasData = true; 16358 } 16359 /** Restores the indexed binding after external rendering without uploading unchanged data. */ 16360 bind() { 16361 if (!this.bindingDirty) return; 16362 const gl = this.context.gl; 16363 gl.bindBufferBase(gl.UNIFORM_BUFFER, this.binding, this.buffer); 16364 this.bindingDirty = false; 16365 } 16366 destroy() { 16367 const gl = this.context.gl; 16368 if (this.buffer) { 16369 gl.deleteBuffer(this.buffer); 16370 delete this.buffer; 16371 } 16372 } 16373}; 16374//#endregion 16375//#region src/webgl/projection_uniform_buffer.ts 16376const layout$2 = std140Layout([ 16377 { 16378 name: "u_projection_matrix", 16379 type: "mat4" 16380 }, 16381 { 16382 name: "u_projection_fallback_matrix", 16383 type: "mat4" 16384 }, 16385 { 16386 name: "u_projection_tile_mercator_coords", 16387 type: "vec4" 16388 }, 16389 { 16390 name: "u_projection_clipping_plane", 16391 type: "vec4" 16392 }, 16393 { 16394 name: "u_projection_transition", 16395 type: "float" 16396 }, 16397 { 16398 name: "u_projection_clip_antimeridian", 16399 type: "int" 16400 } 16401]); 16402const offsets$2 = layout$2.offsets; 16403/** 16404* @internal 16405* The buffer behind the `ProjectionUBO` block in the shader preludes, written by `Program.draw` before each draw. 16406*/ 16407function createProjectionUniformBuffer(context) { 16408 return new UniformBuffer(context, UBO_BINDINGS.ProjectionUBO, layout$2); 16409} 16410function updateProjectionUniformBuffer(buffer, projectionData) { 16411 const f32 = buffer.pending; 16412 f32.set(projectionData.mainMatrix, offsets$2.u_projection_matrix); 16413 f32.set(projectionData.fallbackMatrix, offsets$2.u_projection_fallback_matrix); 16414 f32.set(projectionData.tileMercatorCoords, offsets$2.u_projection_tile_mercator_coords); 16415 f32.set(projectionData.clippingPlane, offsets$2.u_projection_clipping_plane); 16416 f32[offsets$2.u_projection_transition] = projectionData.projectionTransition; 16417 buffer.pendingWords[offsets$2.u_projection_clip_antimeridian] = projectionData.clipAntimeridian ? 1 : 0; 16418 buffer.upload(); 16419} 16420//#endregion 16421//#region src/webgl/terrain_uniform_buffer.ts 16422const layout$1 = std140Layout([ 16423 { 16424 name: "u_terrain_matrix", 16425 type: "mat4" 16426 }, 16427 { 16428 name: "u_terrain_unpack", 16429 type: "vec4" 16430 }, 16431 { 16432 name: "u_terrain_dim", 16433 type: "float" 16434 }, 16435 { 16436 name: "u_terrain_exaggeration", 16437 type: "float" 16438 } 16439]); 16440const offsets$1 = layout$1.offsets; 16441/** 16442* @internal 16443* The buffer behind the `TerrainUBO` block in the vertex prelude, written by `Program.draw` for every draw with terrain. 16444*/ 16445function createTerrainUniformBuffer(context) { 16446 return new UniformBuffer(context, UBO_BINDINGS.TerrainUBO, layout$1); 16447} 16448function updateTerrainUniformBuffer(buffer, terrain) { 16449 const f32 = buffer.pending; 16450 f32.set(terrain.u_terrain_matrix, offsets$1.u_terrain_matrix); 16451 f32.set(terrain.u_terrain_unpack, offsets$1.u_terrain_unpack); 16452 f32[offsets$1.u_terrain_dim] = terrain.u_terrain_dim; 16453 f32[offsets$1.u_terrain_exaggeration] = terrain.u_terrain_exaggeration; 16454 buffer.upload(); 16455} 16456//#endregion 16457//#region src/webgl/program.ts 16458function getTokenizedAttributesAndUniforms(array) { 16459 const result = []; 16460 for (const entry of array) { 16461 if (entry === null) continue; 16462 const token = entry.split(" "); 16463 result.push(token.pop()); 16464 } 16465 return result; 16466} 16467function getIntegerAttributeNames(gl, program) { 16468 const integerTypes = /* @__PURE__ */ new Set([ 16469 gl.INT, 16470 gl.INT_VEC2, 16471 gl.INT_VEC3, 16472 gl.INT_VEC4, 16473 gl.UNSIGNED_INT, 16474 gl.UNSIGNED_INT_VEC2, 16475 gl.UNSIGNED_INT_VEC3, 16476 gl.UNSIGNED_INT_VEC4 16477 ]); 16478 const names = /* @__PURE__ */ new Set(); 16479 const numActiveAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); 16480 for (let i = 0; i < numActiveAttributes; i++) { 16481 const attribute = gl.getActiveAttrib(program, i); 16482 if (attribute && integerTypes.has(attribute.type)) names.add(attribute.name); 16483 } 16484 return names; 16485} 16486/** 16487* @internal 16488* A webgl program to execute in the GPU space 16489*/ 16490var Program = class { 16491 constructor(context, source, configuration, fixedUniforms, showOverdrawInspector, useTerrain, projectionPrelude, projectionDefine, extraDefines = []) { 16492 const gl = context.gl; 16493 this.program = gl.createProgram(); 16494 const staticAttrInfo = getTokenizedAttributesAndUniforms(source.staticAttributes); 16495 const dynamicAttrInfo = configuration ? configuration.getBinderAttributes() : []; 16496 const allAttrInfo = staticAttrInfo.concat(dynamicAttrInfo); 16497 const preludeUniformsInfo = shaders.prelude.staticUniforms ? getTokenizedAttributesAndUniforms(shaders.prelude.staticUniforms) : []; 16498 const staticUniformsInfo = source.staticUniforms ? getTokenizedAttributesAndUniforms(source.staticUniforms) : []; 16499 const dynamicUniformsInfo = configuration ? configuration.getBinderUniforms() : []; 16500 const uniformList = preludeUniformsInfo.concat(staticUniformsInfo).concat(dynamicUniformsInfo); 16501 const allUniformsInfo = []; 16502 for (const uniform of uniformList) if (!allUniformsInfo.includes(uniform)) allUniformsInfo.push(uniform); 16503 const defines = configuration ? configuration.defines() : []; 16504 defines.unshift("#version 300 es"); 16505 if (showOverdrawInspector) defines.push("#define OVERDRAW_INSPECTOR;"); 16506 if (useTerrain) defines.push("#define TERRAIN3D;"); 16507 if (projectionDefine) defines.push(projectionDefine); 16508 if (extraDefines) defines.push(...extraDefines); 16509 const fragmentSource = defines.concat(shaders.prelude.fragmentSource, projectionPrelude.fragmentSource, source.fragmentSource).join("\n"); 16510 const vertexSource = defines.concat(shaders.prelude.vertexSource, projectionPrelude.vertexSource, source.vertexSource).join("\n"); 16511 const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 16512 if (gl.isContextLost()) { 16513 this.failedToCreate = true; 16514 return; 16515 } 16516 gl.shaderSource(fragmentShader, fragmentSource); 16517 gl.compileShader(fragmentShader); 16518 gl.attachShader(this.program, fragmentShader); 16519 const vertexShader = gl.createShader(gl.VERTEX_SHADER); 16520 if (gl.isContextLost()) { 16521 this.failedToCreate = true; 16522 return; 16523 } 16524 gl.shaderSource(vertexShader, vertexSource); 16525 gl.compileShader(vertexShader); 16526 gl.attachShader(this.program, vertexShader); 16527 this.attributes = {}; 16528 const uniformLocations = {}; 16529 this.numAttributes = allAttrInfo.length; 16530 gl.linkProgram(this.program); 16531 if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 16532 if (gl.isContextLost()) { 16533 this.failedToCreate = true; 16534 return; 16535 } 16536 if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) throw new Error(`Could not compile fragment shader: ${gl.getShaderInfoLog(fragmentShader)}`); 16537 if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) throw new Error(`Could not compile vertex shader: ${gl.getShaderInfoLog(vertexShader)}`); 16538 throw new Error(`Program failed to link: ${gl.getProgramInfoLog(this.program)}`); 16539 } 16540 applyUBOBindings(gl, this.program); 16541 const integerAttributeNames = getIntegerAttributeNames(gl, this.program); 16542 for (const name of allAttrInfo) { 16543 if (!name) continue; 16544 const location = gl.getAttribLocation(this.program, name); 16545 if (location >= 0) this.attributes[name] = { 16546 location, 16547 isInteger: integerAttributeNames.has(name) 16548 }; 16549 } 16550 gl.deleteShader(vertexShader); 16551 gl.deleteShader(fragmentShader); 16552 for (const uniform of allUniformsInfo) if (uniform && !uniformLocations[uniform]) { 16553 const uniformLocation = gl.getUniformLocation(this.program, uniform); 16554 if (uniformLocation) uniformLocations[uniform] = uniformLocation; 16555 } 16556 this.fixedUniforms = fixedUniforms(context, uniformLocations); 16557 this.terrainUniforms = terrainPreludeUniforms(context, uniformLocations); 16558 this.binderUniforms = configuration ? configuration.getUniforms(context, uniformLocations) : []; 16559 } 16560 draw(context, drawMode, depthMode, stencilMode, colorMode, cullFaceMode, uniformValues, terrain, projectionData, layerID, layoutVertexBuffer, indexBuffer, segments, currentProperties, zoom, configuration, dynamicLayoutBuffer, dynamicLayoutBuffer2, dynamicLayoutBuffer3) { 16561 const gl = context.gl; 16562 if (this.failedToCreate) return; 16563 context.program.set(this.program); 16564 context.projectionUniformBuffer.bind(); 16565 context.terrainUniformBuffer.bind(); 16566 context.frameUniformBuffer.bind(); 16567 context.setDepthMode(depthMode); 16568 context.setStencilMode(stencilMode); 16569 context.setColorMode(colorMode); 16570 context.setCullFace(cullFaceMode); 16571 if (terrain) { 16572 context.activeTexture.set(gl.TEXTURE2); 16573 gl.bindTexture(gl.TEXTURE_2D, terrain.depthTexture); 16574 context.activeTexture.set(gl.TEXTURE3); 16575 gl.bindTexture(gl.TEXTURE_2D, terrain.texture); 16576 for (const name in this.terrainUniforms) this.terrainUniforms[name].set(terrain[name]); 16577 updateTerrainUniformBuffer(context.terrainUniformBuffer, terrain); 16578 } 16579 if (projectionData) updateProjectionUniformBuffer(context.projectionUniformBuffer, projectionData); 16580 if (uniformValues) for (const name in this.fixedUniforms) this.fixedUniforms[name].set(uniformValues[name]); 16581 if (configuration) configuration.setUniforms(context, this.binderUniforms, currentProperties, { zoom }); 16582 let primitiveSize = 0; 16583 switch (drawMode) { 16584 case gl.LINES: 16585 primitiveSize = 2; 16586 break; 16587 case gl.TRIANGLES: 16588 primitiveSize = 3; 16589 break; 16590 case gl.LINE_STRIP: primitiveSize = 1; 16591 } 16592 for (const segment of segments.get()) { 16593 segment.vaos ||= {}; 16594 segment.vaos[layerID] ||= new VertexArrayObject(); 16595 segment.vaos[layerID].bind(context, this, layoutVertexBuffer, configuration ? configuration.getPaintVertexBuffers() : [], indexBuffer, segment.vertexOffset, dynamicLayoutBuffer, dynamicLayoutBuffer2, dynamicLayoutBuffer3); 16596 gl.drawElements(drawMode, segment.primitiveLength * primitiveSize, gl.UNSIGNED_SHORT, segment.primitiveOffset * primitiveSize * 2); 16597 } 16598 } 16599}; 16600//#endregion 16601//#region src/webgl/program/pattern.ts 16602function patternUniformValues(crossfade, painter, tile) { 16603 const tileRatio = 1 / pixelsToTileUnits(tile, 1, painter.transform.tileZoom); 16604 const numTiles = Math.pow(2, tile.tileID.overscaledZ); 16605 const tileSizeAtNearestZoom = tile.tileSize * Math.pow(2, painter.transform.tileZoom) / numTiles; 16606 const pixelX = tileSizeAtNearestZoom * (tile.tileID.canonical.x + tile.tileID.wrap * numTiles); 16607 const pixelY = tileSizeAtNearestZoom * tile.tileID.canonical.y; 16608 return { 16609 "u_image": 0, 16610 "u_texsize": tile.imageAtlasTexture.size, 16611 "u_scale": [ 16612 tileRatio, 16613 crossfade.fromScale, 16614 crossfade.toScale 16615 ], 16616 "u_fade": crossfade.t, 16617 "u_pixel_coord_upper": [pixelX >> 16, pixelY >> 16], 16618 "u_pixel_coord_lower": [pixelX & 65535, pixelY & 65535] 16619 }; 16620} 16621function bgPatternUniformValues(image, crossfade, painter, tile) { 16622 const imagePosA = painter.patternAtlas.getPattern(image.from.toString()); 16623 const imagePosB = painter.patternAtlas.getPattern(image.to.toString()); 16624 const { width, height } = painter.patternAtlas.getPixelSize(); 16625 const numTiles = Math.pow(2, tile.tileID.overscaledZ); 16626 const tileSizeAtNearestZoom = tile.tileSize * Math.pow(2, painter.transform.tileZoom) / numTiles; 16627 const pixelX = tileSizeAtNearestZoom * (tile.tileID.canonical.x + tile.tileID.wrap * numTiles); 16628 const pixelY = tileSizeAtNearestZoom * tile.tileID.canonical.y; 16629 return { 16630 "u_image": 0, 16631 "u_pattern_tl_a": imagePosA.tl, 16632 "u_pattern_br_a": imagePosA.br, 16633 "u_pattern_tl_b": imagePosB.tl, 16634 "u_pattern_br_b": imagePosB.br, 16635 "u_texsize": [width, height], 16636 "u_mix": crossfade.t, 16637 "u_pattern_size_a": imagePosA.displaySize, 16638 "u_pattern_size_b": imagePosB.displaySize, 16639 "u_scale_a": crossfade.fromScale, 16640 "u_scale_b": crossfade.toScale, 16641 "u_tile_units_to_pixels": 1 / pixelsToTileUnits(tile, 1, painter.transform.tileZoom), 16642 "u_pixel_coord_upper": [pixelX >> 16, pixelY >> 16], 16643 "u_pixel_coord_lower": [pixelX & 65535, pixelY & 65535] 16644 }; 16645} 16646//#endregion 16647//#region src/webgl/program/fill_extrusion_program.ts 16648const fillExtrusionUniforms = (context, locations) => ({ 16649 "u_lightpos": new Uniform3f(context, locations.u_lightpos), 16650 "u_lightpos_globe": new Uniform3f(context, locations.u_lightpos_globe), 16651 "u_lightintensity": new Uniform1f(context, locations.u_lightintensity), 16652 "u_lightcolor": new Uniform3f(context, locations.u_lightcolor), 16653 "u_vertical_gradient": new Uniform1f(context, locations.u_vertical_gradient), 16654 "u_opacity": new Uniform1f(context, locations.u_opacity), 16655 "u_fill_translate": new Uniform2f(context, locations.u_fill_translate) 16656}); 16657const fillExtrusionPatternUniforms = (context, locations) => ({ 16658 "u_lightpos": new Uniform3f(context, locations.u_lightpos), 16659 "u_lightpos_globe": new Uniform3f(context, locations.u_lightpos_globe), 16660 "u_lightintensity": new Uniform1f(context, locations.u_lightintensity), 16661 "u_lightcolor": new Uniform3f(context, locations.u_lightcolor), 16662 "u_vertical_gradient": new Uniform1f(context, locations.u_vertical_gradient), 16663 "u_height_factor": new Uniform1f(context, locations.u_height_factor), 16664 "u_opacity": new Uniform1f(context, locations.u_opacity), 16665 "u_fill_translate": new Uniform2f(context, locations.u_fill_translate), 16666 "u_image": new Uniform1i(context, locations.u_image), 16667 "u_texsize": new Uniform2f(context, locations.u_texsize), 16668 "u_pixel_coord_upper": new Uniform2f(context, locations.u_pixel_coord_upper), 16669 "u_pixel_coord_lower": new Uniform2f(context, locations.u_pixel_coord_lower), 16670 "u_scale": new Uniform3f(context, locations.u_scale), 16671 "u_fade": new Uniform1f(context, locations.u_fade) 16672}); 16673const fillExtrusionUniformValues = (painter, shouldUseVerticalGradient, opacity, translate) => { 16674 const light = painter.style.light; 16675 const lightPos = light.getCartesianPosition(); 16676 const lightMat = create$4(); 16677 if (light.properties.get("anchor") === "viewport") fromRotation(lightMat, painter.transform.bearingInRadians); 16678 transformMat3(lightPos, lightPos, lightMat); 16679 const transformedLightPos = painter.transform.transformLightDirection(lightPos); 16680 const lightColor = light.properties.get("color"); 16681 return { 16682 "u_lightpos": lightPos, 16683 "u_lightpos_globe": transformedLightPos, 16684 "u_lightintensity": light.properties.get("intensity"), 16685 "u_lightcolor": [ 16686 lightColor.r, 16687 lightColor.g, 16688 lightColor.b 16689 ], 16690 "u_vertical_gradient": +shouldUseVerticalGradient, 16691 "u_opacity": opacity, 16692 "u_fill_translate": translate 16693 }; 16694}; 16695const fillExtrusionPatternUniformValues = (painter, shouldUseVerticalGradient, opacity, translate, coord, crossfade, tile) => { 16696 return extend(fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate), patternUniformValues(crossfade, painter, tile), { "u_height_factor": -Math.pow(2, coord.overscaledZ) / tile.tileSize / 8 }); 16697}; 16698//#endregion 16699//#region src/webgl/program/fill_program.ts 16700const fillUniforms = (context, locations) => ({ "u_fill_translate": new Uniform2f(context, locations.u_fill_translate) }); 16701const fillPatternUniforms = (context, locations) => ({ 16702 "u_image": new Uniform1i(context, locations.u_image), 16703 "u_texsize": new Uniform2f(context, locations.u_texsize), 16704 "u_pixel_coord_upper": new Uniform2f(context, locations.u_pixel_coord_upper), 16705 "u_pixel_coord_lower": new Uniform2f(context, locations.u_pixel_coord_lower), 16706 "u_scale": new Uniform3f(context, locations.u_scale), 16707 "u_fade": new Uniform1f(context, locations.u_fade), 16708 "u_sdf_pattern": new Uniform1i(context, locations.u_sdf_pattern), 16709 "u_fill_translate": new Uniform2f(context, locations.u_fill_translate) 16710}); 16711const fillOutlineUniforms = (context, locations) => ({ "u_fill_translate": new Uniform2f(context, locations.u_fill_translate) }); 16712const fillOutlinePatternUniforms = (context, locations) => ({ 16713 "u_image": new Uniform1i(context, locations.u_image), 16714 "u_texsize": new Uniform2f(context, locations.u_texsize), 16715 "u_pixel_coord_upper": new Uniform2f(context, locations.u_pixel_coord_upper), 16716 "u_pixel_coord_lower": new Uniform2f(context, locations.u_pixel_coord_lower), 16717 "u_scale": new Uniform3f(context, locations.u_scale), 16718 "u_fade": new Uniform1f(context, locations.u_fade), 16719 "u_sdf_pattern": new Uniform1i(context, locations.u_sdf_pattern), 16720 "u_fill_translate": new Uniform2f(context, locations.u_fill_translate) 16721}); 16722const fillPatternUniformValues = (painter, crossfade, tile, translate, isSdfPattern) => extend(patternUniformValues(crossfade, painter, tile), { 16723 "u_fill_translate": translate, 16724 "u_sdf_pattern": isSdfPattern ? 1 : 0 16725}); 16726const fillUniformValues = (translate) => ({ "u_fill_translate": translate }); 16727const fillOutlineUniformValues = (translate) => ({ "u_fill_translate": translate }); 16728const fillOutlinePatternUniformValues = (painter, crossfade, tile, translate, isSdfPattern) => fillPatternUniformValues(painter, crossfade, tile, translate, isSdfPattern); 16729//#endregion 16730//#region src/webgl/program/circle_program.ts 16731const circleUniforms = (context, locations) => ({ 16732 "u_scale_with_map": new Uniform1i(context, locations.u_scale_with_map), 16733 "u_pitch_with_map": new Uniform1i(context, locations.u_pitch_with_map), 16734 "u_extrude_scale": new Uniform2f(context, locations.u_extrude_scale), 16735 "u_globe_extrude_scale": new Uniform1f(context, locations.u_globe_extrude_scale), 16736 "u_translate": new Uniform2f(context, locations.u_translate) 16737}); 16738const circleUniformValues = (painter, tile, layer, translate, radiusCorrectionFactor) => { 16739 const transform = painter.transform; 16740 let pitchWithMap, extrudeScale; 16741 let globeExtrudeScale = 0; 16742 if (layer.paint.get("circle-pitch-alignment") === "map") { 16743 const pixelRatio = pixelsToTileUnits(tile, 1, transform.zoom); 16744 pitchWithMap = true; 16745 extrudeScale = [pixelRatio, pixelRatio]; 16746 globeExtrudeScale = pixelRatio / (EXTENT * Math.pow(2, tile.tileID.overscaledZ)) * 2 * Math.PI * radiusCorrectionFactor; 16747 } else { 16748 pitchWithMap = false; 16749 extrudeScale = transform.pixelsToGLUnits; 16750 } 16751 return { 16752 "u_scale_with_map": +(layer.paint.get("circle-pitch-scale") === "map"), 16753 "u_pitch_with_map": +pitchWithMap, 16754 "u_extrude_scale": extrudeScale, 16755 "u_globe_extrude_scale": globeExtrudeScale, 16756 "u_translate": translate 16757 }; 16758}; 16759//#endregion 16760//#region src/webgl/program/debug_program.ts 16761const debugUniforms = (context, locations) => ({ 16762 "u_color": new UniformColor(context, locations.u_color), 16763 "u_overlay": new Uniform1i(context, locations.u_overlay), 16764 "u_overlay_scale": new Uniform1f(context, locations.u_overlay_scale) 16765}); 16766const debugUniformValues = (color, scaleRatio = 1) => ({ 16767 "u_color": color, 16768 "u_overlay": 0, 16769 "u_overlay_scale": scaleRatio 16770}); 16771//#endregion 16772//#region src/webgl/program/heatmap_program.ts 16773const heatmapUniforms = (context, locations) => ({ 16774 "u_extrude_scale": new Uniform1f(context, locations.u_extrude_scale), 16775 "u_intensity": new Uniform1f(context, locations.u_intensity), 16776 "u_globe_extrude_scale": new Uniform1f(context, locations.u_globe_extrude_scale) 16777}); 16778const heatmapTextureUniforms = (context, locations) => ({ 16779 "u_matrix": new UniformMatrix4f(context, locations.u_matrix), 16780 "u_image": new Uniform1i(context, locations.u_image), 16781 "u_color_ramp": new Uniform1i(context, locations.u_color_ramp), 16782 "u_opacity": new Uniform1f(context, locations.u_opacity) 16783}); 16784const heatmapUniformValues = (tile, zoom, intensity, radiusCorrectionFactor) => { 16785 const globeExtrudeScale = pixelsToTileUnits(tile, 1, zoom) / (EXTENT * Math.pow(2, tile.tileID.overscaledZ)) * 2 * Math.PI * radiusCorrectionFactor; 16786 return { 16787 "u_extrude_scale": pixelsToTileUnits(tile, 1, zoom), 16788 "u_intensity": intensity, 16789 "u_globe_extrude_scale": globeExtrudeScale 16790 }; 16791}; 16792const heatmapTextureUniformValues = (painter, layer, textureUnit, colorRampUnit) => { 16793 const matrix = create$1(); 16794 ortho(matrix, 0, painter.width, painter.height, 0, 0, 1); 16795 return { 16796 "u_matrix": matrix, 16797 "u_image": textureUnit, 16798 "u_color_ramp": colorRampUnit, 16799 "u_opacity": layer.paint.get("heatmap-opacity") 16800 }; 16801}; 16802//#endregion 16803//#region src/webgl/program/hillshade_program.ts 16804const hillshadeUniforms = (context, locations) => ({ 16805 "u_image": new Uniform1i(context, locations.u_image), 16806 "u_latrange": new Uniform2f(context, locations.u_latrange), 16807 "u_exaggeration": new Uniform1f(context, locations.u_exaggeration), 16808 "u_altitudes": new UniformFloatArray(context, locations.u_altitudes), 16809 "u_azimuths": new UniformFloatArray(context, locations.u_azimuths), 16810 "u_accent": new UniformColor(context, locations.u_accent), 16811 "u_method": new Uniform1i(context, locations.u_method), 16812 "u_shadows": new UniformColorArray(context, locations.u_shadows), 16813 "u_highlights": new UniformColorArray(context, locations.u_highlights) 16814}); 16815const hillshadePrepareUniforms = (context, locations) => ({ 16816 "u_matrix": new UniformMatrix4f(context, locations.u_matrix), 16817 "u_image": new Uniform1i(context, locations.u_image), 16818 "u_dimension": new Uniform2f(context, locations.u_dimension), 16819 "u_zoom": new Uniform1f(context, locations.u_zoom), 16820 "u_unpack": new Uniform4f(context, locations.u_unpack) 16821}); 16822const hillshadeUniformValues = (painter, tile, layer) => { 16823 const accent = layer.paint.get("hillshade-accent-color"); 16824 let method; 16825 switch (layer.paint.get("hillshade-method")) { 16826 case "basic": 16827 method = 4; 16828 break; 16829 case "combined": 16830 method = 1; 16831 break; 16832 case "igor": 16833 method = 2; 16834 break; 16835 case "multidirectional": 16836 method = 3; 16837 break; 16838 default: method = 0; 16839 } 16840 const illumination = layer.getIlluminationProperties(); 16841 for (let i = 0; i < illumination.directionRadians.length; i++) if (layer.paint.get("hillshade-illumination-anchor") === "viewport") illumination.directionRadians[i] += painter.transform.bearingInRadians; 16842 return { 16843 "u_image": 0, 16844 "u_latrange": getTileLatRange(painter, tile.tileID), 16845 "u_exaggeration": layer.paint.get("hillshade-exaggeration"), 16846 "u_altitudes": illumination.altitudeRadians, 16847 "u_azimuths": illumination.directionRadians, 16848 "u_accent": accent, 16849 "u_method": method, 16850 "u_highlights": illumination.highlightColor, 16851 "u_shadows": illumination.shadowColor 16852 }; 16853}; 16854const hillshadeUniformPrepareValues = (tileID, dem) => { 16855 const stride = dem.stride; 16856 const matrix = create$1(); 16857 ortho(matrix, 0, EXTENT, -EXTENT, 0, 0, 1); 16858 translate(matrix, matrix, [ 16859 0, 16860 -EXTENT, 16861 0 16862 ]); 16863 return { 16864 "u_matrix": matrix, 16865 "u_image": 1, 16866 "u_dimension": [stride, stride], 16867 "u_zoom": tileID.overscaledZ, 16868 "u_unpack": dem.getUnpackVector() 16869 }; 16870}; 16871function getTileLatRange(painter, tileID) { 16872 const tilesAtZoom = Math.pow(2, tileID.canonical.z); 16873 const y = tileID.canonical.y; 16874 return [new MercatorCoordinate(0, y / tilesAtZoom).toLngLat().lat, new MercatorCoordinate(0, (y + 1) / tilesAtZoom).toLngLat().lat]; 16875} 16876//#endregion 16877//#region src/webgl/program/color_relief_program.ts 16878const colorReliefUniforms = (context, locations) => ({ 16879 "u_image": new Uniform1i(context, locations.u_image), 16880 "u_unpack": new Uniform4f(context, locations.u_unpack), 16881 "u_dimension": new Uniform2f(context, locations.u_dimension), 16882 "u_elevation_stops": new Uniform1i(context, locations.u_elevation_stops), 16883 "u_color_stops": new Uniform1i(context, locations.u_color_stops), 16884 "u_color_ramp_size": new Uniform1i(context, locations.u_color_ramp_size), 16885 "u_opacity": new Uniform1f(context, locations.u_opacity) 16886}); 16887const colorReliefUniformValues = (layer, dem, colorRampSize = 0) => { 16888 return { 16889 "u_image": 0, 16890 "u_unpack": dem.getUnpackVector(), 16891 "u_dimension": [dem.stride, dem.stride], 16892 "u_elevation_stops": 1, 16893 "u_color_stops": 4, 16894 "u_color_ramp_size": colorRampSize, 16895 "u_opacity": layer.paint.get("color-relief-opacity") 16896 }; 16897}; 16898//#endregion 16899//#region src/webgl/program/line_program.ts 16900const lineUniforms = (context, locations) => ({ 16901 "u_translation": new Uniform2f(context, locations.u_translation), 16902 "u_ratio": new Uniform1f(context, locations.u_ratio) 16903}); 16904const lineGradientUniforms = (context, locations) => ({ 16905 "u_translation": new Uniform2f(context, locations.u_translation), 16906 "u_ratio": new Uniform1f(context, locations.u_ratio), 16907 "u_image": new Uniform1i(context, locations.u_image), 16908 "u_image_height": new Uniform1f(context, locations.u_image_height) 16909}); 16910const linePatternUniforms = (context, locations) => ({ 16911 "u_translation": new Uniform2f(context, locations.u_translation), 16912 "u_texsize": new Uniform2f(context, locations.u_texsize), 16913 "u_ratio": new Uniform1f(context, locations.u_ratio), 16914 "u_image": new Uniform1i(context, locations.u_image), 16915 "u_scale": new Uniform3f(context, locations.u_scale), 16916 "u_fade": new Uniform1f(context, locations.u_fade) 16917}); 16918const lineSDFUniforms = (context, locations) => ({ 16919 "u_translation": new Uniform2f(context, locations.u_translation), 16920 "u_ratio": new Uniform1f(context, locations.u_ratio), 16921 "u_image": new Uniform1i(context, locations.u_image), 16922 "u_mix": new Uniform1f(context, locations.u_mix), 16923 "u_tileratio": new Uniform1f(context, locations.u_tileratio), 16924 "u_crossfade_from": new Uniform1f(context, locations.u_crossfade_from), 16925 "u_crossfade_to": new Uniform1f(context, locations.u_crossfade_to), 16926 "u_lineatlas_width": new Uniform1f(context, locations.u_lineatlas_width), 16927 "u_lineatlas_height": new Uniform1f(context, locations.u_lineatlas_height) 16928}); 16929const lineGradientSDFUniforms = (context, locations) => ({ 16930 "u_translation": new Uniform2f(context, locations.u_translation), 16931 "u_ratio": new Uniform1f(context, locations.u_ratio), 16932 "u_image": new Uniform1i(context, locations.u_image), 16933 "u_image_height": new Uniform1f(context, locations.u_image_height), 16934 "u_tileratio": new Uniform1f(context, locations.u_tileratio), 16935 "u_crossfade_from": new Uniform1f(context, locations.u_crossfade_from), 16936 "u_crossfade_to": new Uniform1f(context, locations.u_crossfade_to), 16937 "u_image_dash": new Uniform1i(context, locations.u_image_dash), 16938 "u_mix": new Uniform1f(context, locations.u_mix), 16939 "u_lineatlas_width": new Uniform1f(context, locations.u_lineatlas_width), 16940 "u_lineatlas_height": new Uniform1f(context, locations.u_lineatlas_height) 16941}); 16942const lineUniformValues = (painter, tile, layer, ratioScale) => { 16943 const transform = painter.transform; 16944 return { 16945 "u_translation": calculateTranslation(painter, tile, layer), 16946 "u_ratio": ratioScale / pixelsToTileUnits(tile, 1, transform.zoom) 16947 }; 16948}; 16949const lineGradientUniformValues = (painter, tile, layer, ratioScale, imageHeight) => { 16950 return extend(lineUniformValues(painter, tile, layer, ratioScale), { 16951 "u_image": 0, 16952 "u_image_height": imageHeight 16953 }); 16954}; 16955const linePatternUniformValues = (painter, tile, layer, ratioScale, crossfade) => { 16956 const transform = painter.transform; 16957 const tileZoomRatio = calculateTileRatio(tile, transform); 16958 return { 16959 "u_translation": calculateTranslation(painter, tile, layer), 16960 "u_texsize": tile.imageAtlasTexture.size, 16961 "u_ratio": ratioScale / pixelsToTileUnits(tile, 1, transform.zoom), 16962 "u_image": 0, 16963 "u_scale": [ 16964 tileZoomRatio, 16965 crossfade.fromScale, 16966 crossfade.toScale 16967 ], 16968 "u_fade": crossfade.t 16969 }; 16970}; 16971const lineSDFUniformValues = (painter, tile, layer, ratioScale, crossfade) => { 16972 const transform = painter.transform; 16973 const tileRatio = calculateTileRatio(tile, transform); 16974 return extend(lineUniformValues(painter, tile, layer, ratioScale), { 16975 "u_tileratio": tileRatio, 16976 "u_crossfade_from": crossfade.fromScale, 16977 "u_crossfade_to": crossfade.toScale, 16978 "u_image": 0, 16979 "u_mix": crossfade.t, 16980 "u_lineatlas_width": painter.lineAtlas.width, 16981 "u_lineatlas_height": painter.lineAtlas.height 16982 }); 16983}; 16984const lineGradientSDFUniformValues = (painter, tile, layer, ratioScale, crossfade, imageHeight) => { 16985 const transform = painter.transform; 16986 const tileRatio = calculateTileRatio(tile, transform); 16987 return extend(lineUniformValues(painter, tile, layer, ratioScale), { 16988 "u_image": 0, 16989 "u_image_height": imageHeight, 16990 "u_tileratio": tileRatio, 16991 "u_crossfade_from": crossfade.fromScale, 16992 "u_crossfade_to": crossfade.toScale, 16993 "u_image_dash": 1, 16994 "u_mix": crossfade.t, 16995 "u_lineatlas_width": painter.lineAtlas.width, 16996 "u_lineatlas_height": painter.lineAtlas.height 16997 }); 16998}; 16999function calculateTileRatio(tile, transform) { 17000 return 1 / pixelsToTileUnits(tile, 1, transform.tileZoom); 17001} 17002function calculateTranslation(painter, tile, layer) { 17003 return translatePosition(painter.transform, tile, layer.paint.get("line-translate"), layer.paint.get("line-translate-anchor")); 17004} 17005//#endregion 17006//#region src/webgl/program/layer_opacity_program.ts 17007const layerOpacityUniforms = (context, locations) => ({ 17008 "u_image": new Uniform1i(context, locations.u_image), 17009 "u_opacity": new Uniform1f(context, locations.u_opacity) 17010}); 17011const layerOpacityUniformValues = (opacity, textureUnit) => ({ 17012 "u_image": textureUnit, 17013 "u_opacity": opacity 17014}); 17015//#endregion 17016//#region src/webgl/program/symbol_program.ts 17017const symbolIconUniforms = (context, locations) => ({ 17018 "u_is_size_zoom_constant": new Uniform1i(context, locations.u_is_size_zoom_constant), 17019 "u_is_size_feature_constant": new Uniform1i(context, locations.u_is_size_feature_constant), 17020 "u_size_t": new Uniform1f(context, locations.u_size_t), 17021 "u_size": new Uniform1f(context, locations.u_size), 17022 "u_rotate_symbol": new Uniform1i(context, locations.u_rotate_symbol), 17023 "u_label_plane_matrix": new UniformMatrix4f(context, locations.u_label_plane_matrix), 17024 "u_coord_matrix": new UniformMatrix4f(context, locations.u_coord_matrix), 17025 "u_is_text": new Uniform1i(context, locations.u_is_text), 17026 "u_pitch_with_map": new Uniform1i(context, locations.u_pitch_with_map), 17027 "u_is_along_line": new Uniform1i(context, locations.u_is_along_line), 17028 "u_is_variable_anchor": new Uniform1i(context, locations.u_is_variable_anchor), 17029 "u_texsize": new Uniform2f(context, locations.u_texsize), 17030 "u_texture": new Uniform1i(context, locations.u_texture), 17031 "u_translation": new Uniform2f(context, locations.u_translation), 17032 "u_pitched_scale": new Uniform1f(context, locations.u_pitched_scale), 17033 "u_is_offset": new Uniform1i(context, locations.u_is_offset), 17034 "u_height_anchor_ground": new Uniform1i(context, locations.u_height_anchor_ground) 17035}); 17036const symbolSDFUniforms = (context, locations) => ({ 17037 "u_is_size_zoom_constant": new Uniform1i(context, locations.u_is_size_zoom_constant), 17038 "u_is_size_feature_constant": new Uniform1i(context, locations.u_is_size_feature_constant), 17039 "u_size_t": new Uniform1f(context, locations.u_size_t), 17040 "u_size": new Uniform1f(context, locations.u_size), 17041 "u_rotate_symbol": new Uniform1i(context, locations.u_rotate_symbol), 17042 "u_label_plane_matrix": new UniformMatrix4f(context, locations.u_label_plane_matrix), 17043 "u_coord_matrix": new UniformMatrix4f(context, locations.u_coord_matrix), 17044 "u_is_text": new Uniform1i(context, locations.u_is_text), 17045 "u_pitch_with_map": new Uniform1i(context, locations.u_pitch_with_map), 17046 "u_is_along_line": new Uniform1i(context, locations.u_is_along_line), 17047 "u_is_variable_anchor": new Uniform1i(context, locations.u_is_variable_anchor), 17048 "u_texsize": new Uniform2f(context, locations.u_texsize), 17049 "u_texture": new Uniform1i(context, locations.u_texture), 17050 "u_gamma_scale": new Uniform1f(context, locations.u_gamma_scale), 17051 "u_is_halo": new Uniform1i(context, locations.u_is_halo), 17052 "u_is_plain": new Uniform1i(context, locations.u_is_plain), 17053 "u_translation": new Uniform2f(context, locations.u_translation), 17054 "u_pitched_scale": new Uniform1f(context, locations.u_pitched_scale), 17055 "u_is_offset": new Uniform1i(context, locations.u_is_offset), 17056 "u_height_anchor_ground": new Uniform1i(context, locations.u_height_anchor_ground) 17057}); 17058const symbolTextAndIconUniforms = (context, locations) => ({ 17059 "u_is_size_zoom_constant": new Uniform1i(context, locations.u_is_size_zoom_constant), 17060 "u_is_size_feature_constant": new Uniform1i(context, locations.u_is_size_feature_constant), 17061 "u_size_t": new Uniform1f(context, locations.u_size_t), 17062 "u_size": new Uniform1f(context, locations.u_size), 17063 "u_rotate_symbol": new Uniform1i(context, locations.u_rotate_symbol), 17064 "u_label_plane_matrix": new UniformMatrix4f(context, locations.u_label_plane_matrix), 17065 "u_coord_matrix": new UniformMatrix4f(context, locations.u_coord_matrix), 17066 "u_is_text": new Uniform1i(context, locations.u_is_text), 17067 "u_pitch_with_map": new Uniform1i(context, locations.u_pitch_with_map), 17068 "u_is_along_line": new Uniform1i(context, locations.u_is_along_line), 17069 "u_is_variable_anchor": new Uniform1i(context, locations.u_is_variable_anchor), 17070 "u_texsize": new Uniform2f(context, locations.u_texsize), 17071 "u_texsize_icon": new Uniform2f(context, locations.u_texsize_icon), 17072 "u_texture": new Uniform1i(context, locations.u_texture), 17073 "u_texture_icon": new Uniform1i(context, locations.u_texture_icon), 17074 "u_gamma_scale": new Uniform1f(context, locations.u_gamma_scale), 17075 "u_is_halo": new Uniform1i(context, locations.u_is_halo), 17076 "u_translation": new Uniform2f(context, locations.u_translation), 17077 "u_pitched_scale": new Uniform1f(context, locations.u_pitched_scale), 17078 "u_is_offset": new Uniform1i(context, locations.u_is_offset), 17079 "u_height_anchor_ground": new Uniform1i(context, locations.u_height_anchor_ground) 17080}); 17081const symbolIconUniformValues = (functionType, size, rotateInShader, pitchWithMap, isAlongLine, isVariableAnchor, painter, labelPlaneMatrix, glCoordMatrix, translation, isText, texSize, pitchedScale, isOffset, heightAnchorGround) => { 17082 return { 17083 "u_is_size_zoom_constant": +(functionType === "constant" || functionType === "source"), 17084 "u_is_size_feature_constant": +(functionType === "constant" || functionType === "camera"), 17085 "u_size_t": size ? size.uSizeT : 0, 17086 "u_size": size ? size.uSize : 0, 17087 "u_rotate_symbol": +rotateInShader, 17088 "u_label_plane_matrix": labelPlaneMatrix, 17089 "u_coord_matrix": glCoordMatrix, 17090 "u_is_text": +isText, 17091 "u_pitch_with_map": +pitchWithMap, 17092 "u_is_along_line": isAlongLine, 17093 "u_is_variable_anchor": isVariableAnchor, 17094 "u_texsize": texSize, 17095 "u_texture": 0, 17096 "u_translation": translation, 17097 "u_pitched_scale": pitchedScale, 17098 "u_is_offset": isOffset, 17099 "u_height_anchor_ground": +heightAnchorGround 17100 }; 17101}; 17102const symbolSDFUniformValues = (functionType, size, rotateInShader, pitchWithMap, isAlongLine, isVariableAnchor, painter, labelPlaneMatrix, glCoordMatrix, translation, isText, texSize, isHalo, pitchedScale, isOffset, heightAnchorGround) => { 17103 const transform = painter.transform; 17104 return extend(symbolIconUniformValues(functionType, size, rotateInShader, pitchWithMap, isAlongLine, isVariableAnchor, painter, labelPlaneMatrix, glCoordMatrix, translation, isText, texSize, pitchedScale, isOffset, heightAnchorGround), { 17105 "u_gamma_scale": pitchWithMap ? Math.cos(transform.pitch * Math.PI / 180) * transform.cameraToCenterDistance : 1, 17106 "u_is_halo": isHalo ? 1 : 0, 17107 "u_is_plain": 1 17108 }); 17109}; 17110const symbolTextAndIconUniformValues = (functionType, size, rotateInShader, pitchWithMap, isAlongLine, isVariableAnchor, painter, labelPlaneMatrix, glCoordMatrix, translation, texSizeSDF, texSizeIcon, pitchedScale, isOffset, heightAnchorGround) => { 17111 return extend(symbolSDFUniformValues(functionType, size, rotateInShader, pitchWithMap, isAlongLine, isVariableAnchor, painter, labelPlaneMatrix, glCoordMatrix, translation, true, texSizeSDF, true, pitchedScale, isOffset, heightAnchorGround), { 17112 "u_texsize_icon": texSizeIcon, 17113 "u_texture_icon": 1 17114 }); 17115}; 17116//#endregion 17117//#region src/webgl/program/background_program.ts 17118const backgroundUniforms = (context, locations) => ({ 17119 "u_opacity": new Uniform1f(context, locations.u_opacity), 17120 "u_color": new UniformColor(context, locations.u_color) 17121}); 17122const backgroundPatternUniforms = (context, locations) => ({ 17123 "u_opacity": new Uniform1f(context, locations.u_opacity), 17124 "u_image": new Uniform1i(context, locations.u_image), 17125 "u_pattern_tl_a": new Uniform2f(context, locations.u_pattern_tl_a), 17126 "u_pattern_br_a": new Uniform2f(context, locations.u_pattern_br_a), 17127 "u_pattern_tl_b": new Uniform2f(context, locations.u_pattern_tl_b), 17128 "u_pattern_br_b": new Uniform2f(context, locations.u_pattern_br_b), 17129 "u_texsize": new Uniform2f(context, locations.u_texsize), 17130 "u_mix": new Uniform1f(context, locations.u_mix), 17131 "u_pattern_size_a": new Uniform2f(context, locations.u_pattern_size_a), 17132 "u_pattern_size_b": new Uniform2f(context, locations.u_pattern_size_b), 17133 "u_scale_a": new Uniform1f(context, locations.u_scale_a), 17134 "u_scale_b": new Uniform1f(context, locations.u_scale_b), 17135 "u_pixel_coord_upper": new Uniform2f(context, locations.u_pixel_coord_upper), 17136 "u_pixel_coord_lower": new Uniform2f(context, locations.u_pixel_coord_lower), 17137 "u_tile_units_to_pixels": new Uniform1f(context, locations.u_tile_units_to_pixels) 17138}); 17139const backgroundUniformValues = (opacity, color) => ({ 17140 "u_opacity": opacity, 17141 "u_color": color 17142}); 17143const backgroundPatternUniformValues = (opacity, painter, image, tile, crossfade) => extend(bgPatternUniformValues(image, crossfade, painter, tile), { "u_opacity": opacity }); 17144//#endregion 17145//#region src/webgl/program/atmosphere_program.ts 17146const atmosphereUniforms = (context, locations) => ({ 17147 "u_sun_pos": new Uniform3f(context, locations.u_sun_pos), 17148 "u_atmosphere_blend": new Uniform1f(context, locations.u_atmosphere_blend), 17149 "u_globe_position": new Uniform3f(context, locations.u_globe_position), 17150 "u_globe_radius": new Uniform1f(context, locations.u_globe_radius), 17151 "u_inv_proj_matrix": new UniformMatrix4f(context, locations.u_inv_proj_matrix) 17152}); 17153const atmosphereUniformValues = (sunPos, atmosphereBlend, globePosition, globeRadius, invProjMatrix) => ({ 17154 "u_sun_pos": sunPos, 17155 "u_atmosphere_blend": atmosphereBlend, 17156 "u_globe_position": globePosition, 17157 "u_globe_radius": globeRadius, 17158 "u_inv_proj_matrix": invProjMatrix 17159}); 17160//#endregion 17161//#region src/webgl/program/sky_program.ts 17162const skyUniforms = (context, locations) => ({ 17163 "u_sky_color": new UniformColor(context, locations.u_sky_color), 17164 "u_horizon_color": new UniformColor(context, locations.u_horizon_color), 17165 "u_horizon": new Uniform2f(context, locations.u_horizon), 17166 "u_horizon_normal": new Uniform2f(context, locations.u_horizon_normal), 17167 "u_sky_horizon_blend": new Uniform1f(context, locations.u_sky_horizon_blend), 17168 "u_sky_blend": new Uniform1f(context, locations.u_sky_blend), 17169 "u_inv_proj_matrix": new UniformMatrix4f(context, locations.u_inv_proj_matrix), 17170 "u_globe_position": new Uniform3f(context, locations.u_globe_position), 17171 "u_globe_radius": new Uniform1f(context, locations.u_globe_radius), 17172 "u_atmosphere_blend": new Uniform1f(context, locations.u_atmosphere_blend) 17173}); 17174const skyUniformValues = (sky, transform, pixelRatio) => { 17175 const cosRoll = Math.cos(transform.rollInRadians); 17176 const sinRoll = Math.sin(transform.rollInRadians); 17177 const mercatorHorizon = getMercatorHorizon(transform); 17178 const skyBlend = transform.getProjectionData({ 17179 overscaledTileID: null, 17180 applyGlobeMatrix: true, 17181 applyTerrainMatrix: true 17182 }).projectionTransition; 17183 const globePosition = getGlobeCenterInViewSpace(transform); 17184 const globeRadius = getGlobeRadiusPixels(transform.worldSize, transform.center.lat); 17185 return { 17186 "u_sky_color": sky.properties.get("sky-color"), 17187 "u_horizon_color": sky.properties.get("horizon-color"), 17188 "u_horizon": [(transform.width / 2 - mercatorHorizon * sinRoll) * pixelRatio, (transform.height / 2 + mercatorHorizon * cosRoll) * pixelRatio], 17189 "u_horizon_normal": [-sinRoll, cosRoll], 17190 "u_sky_horizon_blend": sky.properties.get("sky-horizon-blend") * transform.height / 2 * pixelRatio, 17191 "u_sky_blend": skyBlend, 17192 "u_inv_proj_matrix": transform.inverseProjectionMatrix, 17193 "u_globe_position": globePosition, 17194 "u_globe_radius": globeRadius, 17195 "u_atmosphere_blend": sky.properties.get("atmosphere-blend") * getAtmosphereAltitudeBlend(length(globePosition) - globeRadius, globeRadius) 17196 }; 17197}; 17198//#endregion 17199//#region src/webgl/program/program_uniforms.ts 17200const emptyUniforms = (_, __) => ({}); 17201const programUniforms = { 17202 fillExtrusion: fillExtrusionUniforms, 17203 fillExtrusionPattern: fillExtrusionPatternUniforms, 17204 fill: fillUniforms, 17205 fillPattern: fillPatternUniforms, 17206 fillOutline: fillOutlineUniforms, 17207 fillOutlinePattern: fillOutlinePatternUniforms, 17208 circle: circleUniforms, 17209 collisionBox: emptyUniforms, 17210 collisionCircle: emptyUniforms, 17211 debug: debugUniforms, 17212 depth: emptyUniforms, 17213 clippingMask: emptyUniforms, 17214 heatmap: heatmapUniforms, 17215 heatmapTexture: heatmapTextureUniforms, 17216 hillshade: hillshadeUniforms, 17217 hillshadePrepare: hillshadePrepareUniforms, 17218 colorRelief: colorReliefUniforms, 17219 line: lineUniforms, 17220 lineGradient: lineGradientUniforms, 17221 linePattern: linePatternUniforms, 17222 lineSDF: lineSDFUniforms, 17223 lineGradientSDF: lineGradientSDFUniforms, 17224 layerOpacity: layerOpacityUniforms, 17225 raster: rasterUniforms, 17226 symbolIcon: symbolIconUniforms, 17227 symbolSDF: symbolSDFUniforms, 17228 symbolTextAndIcon: symbolTextAndIconUniforms, 17229 background: backgroundUniforms, 17230 backgroundPattern: backgroundPatternUniforms, 17231 terrain: terrainUniforms, 17232 terrainDepth: terrainDepthUniforms, 17233 atmosphere: atmosphereUniforms, 17234 sky: skyUniforms 17235}; 17236//#endregion 17237//#region src/webgl/index_buffer.ts 17238/** 17239* @internal 17240* an index buffer class 17241*/ 17242var IndexBuffer = class { 17243 constructor(context, array, dynamicDraw) { 17244 this.context = context; 17245 const gl = context.gl; 17246 this.buffer = gl.createBuffer(); 17247 this.dynamicDraw = Boolean(dynamicDraw); 17248 this.context.unbindVAO(); 17249 context.bindElementBuffer.set(this.buffer); 17250 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, array.arrayBuffer, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW); 17251 if (!this.dynamicDraw) array.freeBufferAfterUpload(); 17252 } 17253 bind() { 17254 this.context.bindElementBuffer.set(this.buffer); 17255 } 17256 updateData(array) { 17257 const gl = this.context.gl; 17258 if (!this.dynamicDraw) throw new Error("Attempted to update data while not in dynamic mode."); 17259 this.context.unbindVAO(); 17260 this.bind(); 17261 gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, array.arrayBuffer); 17262 } 17263 destroy() { 17264 const gl = this.context.gl; 17265 if (this.buffer) { 17266 gl.deleteBuffer(this.buffer); 17267 delete this.buffer; 17268 } 17269 } 17270}; 17271//#endregion 17272//#region src/webgl/vertex_buffer.ts 17273/** 17274* An Enum for AttributeType 17275*/ 17276const AttributeType = { 17277 Int8: "BYTE", 17278 Uint8: "UNSIGNED_BYTE", 17279 Int16: "SHORT", 17280 Uint16: "UNSIGNED_SHORT", 17281 Int32: "INT", 17282 Uint32: "UNSIGNED_INT", 17283 Float32: "FLOAT" 17284}; 17285/** 17286* @internal 17287* The `VertexBuffer` class turns a `StructArray` into a WebGL buffer. Each member of the StructArray's 17288* Struct type is converted to a WebGL attribute. 17289*/ 17290var VertexBuffer = class { 17291 /** 17292 * @param dynamicDraw - Whether this buffer will be repeatedly updated. 17293 */ 17294 constructor(context, array, attributes, dynamicDraw) { 17295 this.length = array.length; 17296 this.attributes = attributes; 17297 this.itemSize = array.bytesPerElement; 17298 this.dynamicDraw = dynamicDraw; 17299 this.context = context; 17300 const gl = context.gl; 17301 this.buffer = gl.createBuffer(); 17302 context.bindVertexBuffer.set(this.buffer); 17303 gl.bufferData(gl.ARRAY_BUFFER, array.arrayBuffer, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW); 17304 if (!this.dynamicDraw) array.freeBufferAfterUpload(); 17305 } 17306 bind() { 17307 this.context.bindVertexBuffer.set(this.buffer); 17308 } 17309 updateData(array) { 17310 if (array.length !== this.length) throw new Error(`Length of new data is ${array.length}, which doesn't match current length of ${this.length}`); 17311 const gl = this.context.gl; 17312 this.bind(); 17313 gl.bufferSubData(gl.ARRAY_BUFFER, 0, array.arrayBuffer); 17314 } 17315 enableAttributes(gl, program) { 17316 for (const member of this.attributes) { 17317 const attribute = program.attributes[member.name]; 17318 if (attribute !== void 0) gl.enableVertexAttribArray(attribute.location); 17319 } 17320 } 17321 /** 17322 * Set the attribute pointers in a WebGL context 17323 * @param gl - The WebGL context 17324 * @param program - The active WebGL program 17325 * @param vertexOffset - Index of the starting vertex of the segment 17326 */ 17327 setVertexAttribPointers(gl, program, vertexOffset) { 17328 for (const member of this.attributes) { 17329 const attribute = program.attributes[member.name]; 17330 if (attribute !== void 0) { 17331 const offset = member.offset + this.itemSize * (vertexOffset || 0); 17332 if (attribute.isInteger) gl.vertexAttribIPointer(attribute.location, member.components, gl[AttributeType[member.type]], this.itemSize, offset); 17333 else gl.vertexAttribPointer(attribute.location, member.components, gl[AttributeType[member.type]], false, this.itemSize, offset); 17334 } 17335 } 17336 } 17337 /** 17338 * Destroy the GL buffer bound to the given WebGL context 17339 */ 17340 destroy() { 17341 const gl = this.context.gl; 17342 if (this.buffer) { 17343 gl.deleteBuffer(this.buffer); 17344 delete this.buffer; 17345 } 17346 } 17347}; 17348//#endregion 17349//#region src/webgl/value.ts 17350var BaseValue = class { 17351 constructor(context) { 17352 this.gl = context.gl; 17353 this.default = this.getDefault(); 17354 this.current = this.default; 17355 this.dirty = false; 17356 } 17357 get() { 17358 return this.current; 17359 } 17360 set(value) {} 17361 getDefault() { 17362 return this.default; 17363 } 17364 setDefault() { 17365 this.set(this.default); 17366 } 17367}; 17368var ClearColor = class extends BaseValue { 17369 getDefault() { 17370 return Color.transparent; 17371 } 17372 set(v) { 17373 const c = this.current; 17374 if (v.r === c.r && v.g === c.g && v.b === c.b && v.a === c.a && !this.dirty) return; 17375 this.gl.clearColor(v.r, v.g, v.b, v.a); 17376 this.current = v; 17377 this.dirty = false; 17378 } 17379}; 17380var ClearDepth = class extends BaseValue { 17381 getDefault() { 17382 return 1; 17383 } 17384 set(v) { 17385 if (v === this.current && !this.dirty) return; 17386 this.gl.clearDepth(v); 17387 this.current = v; 17388 this.dirty = false; 17389 } 17390}; 17391var ClearStencil = class extends BaseValue { 17392 getDefault() { 17393 return 0; 17394 } 17395 set(v) { 17396 if (v === this.current && !this.dirty) return; 17397 this.gl.clearStencil(v); 17398 this.current = v; 17399 this.dirty = false; 17400 } 17401}; 17402var ColorMask = class extends BaseValue { 17403 getDefault() { 17404 return [ 17405 true, 17406 true, 17407 true, 17408 true 17409 ]; 17410 } 17411 set(v) { 17412 const c = this.current; 17413 if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && v[3] === c[3] && !this.dirty) return; 17414 this.gl.colorMask(v[0], v[1], v[2], v[3]); 17415 this.current = v; 17416 this.dirty = false; 17417 } 17418}; 17419var DepthMask = class extends BaseValue { 17420 getDefault() { 17421 return true; 17422 } 17423 set(v) { 17424 if (v === this.current && !this.dirty) return; 17425 this.gl.depthMask(v); 17426 this.current = v; 17427 this.dirty = false; 17428 } 17429}; 17430var StencilMask = class extends BaseValue { 17431 getDefault() { 17432 return 255; 17433 } 17434 set(v) { 17435 if (v === this.current && !this.dirty) return; 17436 this.gl.stencilMask(v); 17437 this.current = v; 17438 this.dirty = false; 17439 } 17440}; 17441var StencilFunc = class extends BaseValue { 17442 getDefault() { 17443 return { 17444 func: this.gl.ALWAYS, 17445 ref: 0, 17446 mask: 255 17447 }; 17448 } 17449 set(v) { 17450 const c = this.current; 17451 if (v.func === c.func && v.ref === c.ref && v.mask === c.mask && !this.dirty) return; 17452 this.gl.stencilFunc(v.func, v.ref, v.mask); 17453 this.current = v; 17454 this.dirty = false; 17455 } 17456}; 17457var StencilOp = class extends BaseValue { 17458 getDefault() { 17459 const gl = this.gl; 17460 return [ 17461 gl.KEEP, 17462 gl.KEEP, 17463 gl.KEEP 17464 ]; 17465 } 17466 set(v) { 17467 const c = this.current; 17468 if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && !this.dirty) return; 17469 this.gl.stencilOp(v[0], v[1], v[2]); 17470 this.current = v; 17471 this.dirty = false; 17472 } 17473}; 17474var StencilTest = class extends BaseValue { 17475 getDefault() { 17476 return false; 17477 } 17478 set(v) { 17479 if (v === this.current && !this.dirty) return; 17480 const gl = this.gl; 17481 if (v) gl.enable(gl.STENCIL_TEST); 17482 else gl.disable(gl.STENCIL_TEST); 17483 this.current = v; 17484 this.dirty = false; 17485 } 17486}; 17487var DepthRange = class extends BaseValue { 17488 getDefault() { 17489 return [0, 1]; 17490 } 17491 set(v) { 17492 const c = this.current; 17493 if (v[0] === c[0] && v[1] === c[1] && !this.dirty) return; 17494 this.gl.depthRange(v[0], v[1]); 17495 this.current = v; 17496 this.dirty = false; 17497 } 17498}; 17499var DepthTest = class extends BaseValue { 17500 getDefault() { 17501 return false; 17502 } 17503 set(v) { 17504 if (v === this.current && !this.dirty) return; 17505 const gl = this.gl; 17506 if (v) gl.enable(gl.DEPTH_TEST); 17507 else gl.disable(gl.DEPTH_TEST); 17508 this.current = v; 17509 this.dirty = false; 17510 } 17511}; 17512var DepthFunc = class extends BaseValue { 17513 getDefault() { 17514 return this.gl.LESS; 17515 } 17516 set(v) { 17517 if (v === this.current && !this.dirty) return; 17518 this.gl.depthFunc(v); 17519 this.current = v; 17520 this.dirty = false; 17521 } 17522}; 17523var Blend = class extends BaseValue { 17524 getDefault() { 17525 return false; 17526 } 17527 set(v) { 17528 if (v === this.current && !this.dirty) return; 17529 const gl = this.gl; 17530 if (v) gl.enable(gl.BLEND); 17531 else gl.disable(gl.BLEND); 17532 this.current = v; 17533 this.dirty = false; 17534 } 17535}; 17536var BlendFunc = class extends BaseValue { 17537 getDefault() { 17538 const gl = this.gl; 17539 return [gl.ONE, gl.ZERO]; 17540 } 17541 set(v) { 17542 const c = this.current; 17543 if (v[0] === c[0] && v[1] === c[1] && !this.dirty) return; 17544 this.gl.blendFunc(v[0], v[1]); 17545 this.current = v; 17546 this.dirty = false; 17547 } 17548}; 17549var BlendColor = class extends BaseValue { 17550 getDefault() { 17551 return Color.transparent; 17552 } 17553 set(v) { 17554 const c = this.current; 17555 if (v.r === c.r && v.g === c.g && v.b === c.b && v.a === c.a && !this.dirty) return; 17556 this.gl.blendColor(v.r, v.g, v.b, v.a); 17557 this.current = v; 17558 this.dirty = false; 17559 } 17560}; 17561var BlendEquation = class extends BaseValue { 17562 getDefault() { 17563 return this.gl.FUNC_ADD; 17564 } 17565 set(v) { 17566 if (v === this.current && !this.dirty) return; 17567 this.gl.blendEquation(v); 17568 this.current = v; 17569 this.dirty = false; 17570 } 17571}; 17572var CullFace = class extends BaseValue { 17573 getDefault() { 17574 return false; 17575 } 17576 set(v) { 17577 if (v === this.current && !this.dirty) return; 17578 const gl = this.gl; 17579 if (v) gl.enable(gl.CULL_FACE); 17580 else gl.disable(gl.CULL_FACE); 17581 this.current = v; 17582 this.dirty = false; 17583 } 17584}; 17585var CullFaceSide = class extends BaseValue { 17586 getDefault() { 17587 return this.gl.BACK; 17588 } 17589 set(v) { 17590 if (v === this.current && !this.dirty) return; 17591 this.gl.cullFace(v); 17592 this.current = v; 17593 this.dirty = false; 17594 } 17595}; 17596var FrontFace = class extends BaseValue { 17597 getDefault() { 17598 return this.gl.CCW; 17599 } 17600 set(v) { 17601 if (v === this.current && !this.dirty) return; 17602 this.gl.frontFace(v); 17603 this.current = v; 17604 this.dirty = false; 17605 } 17606}; 17607var ProgramValue = class extends BaseValue { 17608 getDefault() { 17609 return null; 17610 } 17611 set(v) { 17612 if (v === this.current && !this.dirty) return; 17613 this.gl.useProgram(v); 17614 this.current = v; 17615 this.dirty = false; 17616 } 17617}; 17618var ActiveTextureUnit = class extends BaseValue { 17619 getDefault() { 17620 return this.gl.TEXTURE0; 17621 } 17622 set(v) { 17623 if (v === this.current && !this.dirty) return; 17624 this.gl.activeTexture(v); 17625 this.current = v; 17626 this.dirty = false; 17627 } 17628}; 17629var Viewport = class extends BaseValue { 17630 getDefault() { 17631 const gl = this.gl; 17632 return [ 17633 0, 17634 0, 17635 gl.drawingBufferWidth, 17636 gl.drawingBufferHeight 17637 ]; 17638 } 17639 set(v) { 17640 const c = this.current; 17641 if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && v[3] === c[3] && !this.dirty) return; 17642 this.gl.viewport(v[0], v[1], v[2], v[3]); 17643 this.current = v; 17644 this.dirty = false; 17645 } 17646}; 17647var BindFramebuffer = class extends BaseValue { 17648 getDefault() { 17649 return null; 17650 } 17651 set(v) { 17652 if (v === this.current && !this.dirty) return; 17653 const gl = this.gl; 17654 gl.bindFramebuffer(gl.FRAMEBUFFER, v); 17655 this.current = v; 17656 this.dirty = false; 17657 } 17658}; 17659var BindRenderbuffer = class extends BaseValue { 17660 getDefault() { 17661 return null; 17662 } 17663 set(v) { 17664 if (v === this.current && !this.dirty) return; 17665 const gl = this.gl; 17666 gl.bindRenderbuffer(gl.RENDERBUFFER, v); 17667 this.current = v; 17668 this.dirty = false; 17669 } 17670}; 17671var BindTexture = class extends BaseValue { 17672 getDefault() { 17673 return null; 17674 } 17675 set(v) { 17676 if (v === this.current && !this.dirty) return; 17677 const gl = this.gl; 17678 gl.bindTexture(gl.TEXTURE_2D, v); 17679 this.current = v; 17680 this.dirty = false; 17681 } 17682}; 17683var BindVertexBuffer = class extends BaseValue { 17684 getDefault() { 17685 return null; 17686 } 17687 set(v) { 17688 if (v === this.current && !this.dirty) return; 17689 const gl = this.gl; 17690 gl.bindBuffer(gl.ARRAY_BUFFER, v); 17691 this.current = v; 17692 this.dirty = false; 17693 } 17694}; 17695var BindElementBuffer = class extends BaseValue { 17696 getDefault() { 17697 return null; 17698 } 17699 set(v) { 17700 const gl = this.gl; 17701 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, v); 17702 this.current = v; 17703 this.dirty = false; 17704 } 17705}; 17706var BindVertexArray = class extends BaseValue { 17707 getDefault() { 17708 return null; 17709 } 17710 set(v) { 17711 if (v === this.current && !this.dirty) return; 17712 this.gl.bindVertexArray(v); 17713 this.current = v; 17714 this.dirty = false; 17715 } 17716}; 17717var PixelStoreUnpack = class extends BaseValue { 17718 getDefault() { 17719 return 4; 17720 } 17721 set(v) { 17722 if (v === this.current && !this.dirty) return; 17723 const gl = this.gl; 17724 gl.pixelStorei(gl.UNPACK_ALIGNMENT, v); 17725 this.current = v; 17726 this.dirty = false; 17727 } 17728}; 17729var PixelStoreUnpackPremultiplyAlpha = class extends BaseValue { 17730 getDefault() { 17731 return false; 17732 } 17733 set(v) { 17734 if (v === this.current && !this.dirty) return; 17735 const gl = this.gl; 17736 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, v); 17737 this.current = v; 17738 this.dirty = false; 17739 } 17740}; 17741var PixelStoreUnpackFlipY = class extends BaseValue { 17742 getDefault() { 17743 return false; 17744 } 17745 set(v) { 17746 if (v === this.current && !this.dirty) return; 17747 const gl = this.gl; 17748 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, v); 17749 this.current = v; 17750 this.dirty = false; 17751 } 17752}; 17753var FramebufferAttachment = class extends BaseValue { 17754 constructor(context, parent) { 17755 super(context); 17756 this.context = context; 17757 this.parent = parent; 17758 } 17759 getDefault() { 17760 return null; 17761 } 17762}; 17763var ColorAttachment = class extends FramebufferAttachment { 17764 setDirty() { 17765 this.dirty = true; 17766 } 17767 set(v) { 17768 if (v === this.current && !this.dirty) return; 17769 this.context.bindFramebuffer.set(this.parent); 17770 const gl = this.gl; 17771 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v, 0); 17772 this.current = v; 17773 this.dirty = false; 17774 } 17775}; 17776var DepthAttachment = class extends FramebufferAttachment { 17777 set(v) { 17778 if (v === this.current && !this.dirty) return; 17779 this.context.bindFramebuffer.set(this.parent); 17780 const gl = this.gl; 17781 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, v); 17782 this.current = v; 17783 this.dirty = false; 17784 } 17785}; 17786var DepthStencilAttachment = class extends FramebufferAttachment { 17787 set(v) { 17788 if (v === this.current && !this.dirty) return; 17789 this.context.bindFramebuffer.set(this.parent); 17790 const gl = this.gl; 17791 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, v); 17792 this.current = v; 17793 this.dirty = false; 17794 } 17795}; 17796//#endregion 17797//#region src/webgl/framebuffer.ts 17798/** 17799* @internal 17800* A framebuffer holder object 17801*/ 17802var Framebuffer = class { 17803 constructor(context, width, height, hasDepth, hasStencil) { 17804 this.context = context; 17805 this.width = width; 17806 this.height = height; 17807 const gl = context.gl; 17808 const fbo = this.framebuffer = gl.createFramebuffer(); 17809 this.colorAttachment = new ColorAttachment(context, fbo); 17810 if (hasDepth) this.depthAttachment = hasStencil ? new DepthStencilAttachment(context, fbo) : new DepthAttachment(context, fbo); 17811 else if (hasStencil) throw new Error("Stencil cannot be set without depth"); 17812 } 17813 destroy() { 17814 const gl = this.context.gl; 17815 const texture = this.colorAttachment.get(); 17816 if (texture) gl.deleteTexture(texture); 17817 if (this.depthAttachment) { 17818 const renderbuffer = this.depthAttachment.get(); 17819 if (renderbuffer) gl.deleteRenderbuffer(renderbuffer); 17820 } 17821 gl.deleteFramebuffer(this.framebuffer); 17822 } 17823}; 17824//#endregion 17825//#region src/webgl/frame_uniform_buffer.ts 17826const layout = std140Layout([ 17827 { 17828 name: "u_units_to_pixels", 17829 type: "vec2" 17830 }, 17831 { 17832 name: "u_world_size", 17833 type: "vec2" 17834 }, 17835 { 17836 name: "u_camera_to_center_distance", 17837 type: "float" 17838 }, 17839 { 17840 name: "u_symbol_fade_change", 17841 type: "float" 17842 }, 17843 { 17844 name: "u_aspect_ratio", 17845 type: "float" 17846 }, 17847 { 17848 name: "u_device_pixel_ratio", 17849 type: "float" 17850 }, 17851 { 17852 name: "u_viewport_size", 17853 type: "vec2" 17854 }, 17855 { 17856 name: "u_pixel_extrude_scale", 17857 type: "vec2" 17858 }, 17859 { 17860 name: "u_pitch", 17861 type: "float" 17862 } 17863]); 17864const offsets = layout.offsets; 17865/** 17866* @internal 17867* The buffer behind the `FrameUBO` block in the shader preludes, written once per frame by `Painter.render`. 17868*/ 17869function createFrameUniformBuffer(context) { 17870 return new UniformBuffer(context, UBO_BINDINGS.FrameUBO, layout); 17871} 17872function updateFrameUniformBuffer(buffer, painter) { 17873 const f32 = buffer.pending; 17874 const { transform } = painter; 17875 const gl = painter.context.gl; 17876 f32[offsets.u_units_to_pixels] = 1 / transform.pixelsToGLUnits[0]; 17877 f32[offsets.u_units_to_pixels + 1] = 1 / transform.pixelsToGLUnits[1]; 17878 f32[offsets.u_world_size] = gl.drawingBufferWidth; 17879 f32[offsets.u_world_size + 1] = gl.drawingBufferHeight; 17880 f32[offsets.u_camera_to_center_distance] = transform.cameraToCenterDistance; 17881 f32[offsets.u_symbol_fade_change] = painter.options.fadeDuration ? painter.symbolFadeChange : 1; 17882 f32[offsets.u_aspect_ratio] = transform.width / transform.height; 17883 f32[offsets.u_device_pixel_ratio] = painter.pixelRatio; 17884 f32[offsets.u_viewport_size] = transform.width; 17885 f32[offsets.u_viewport_size + 1] = transform.height; 17886 f32[offsets.u_pixel_extrude_scale] = 1 / transform.width; 17887 f32[offsets.u_pixel_extrude_scale + 1] = 1 / transform.height; 17888 f32[offsets.u_pitch] = transform.pitch / 360 * 2 * Math.PI; 17889 buffer.upload(); 17890} 17891//#endregion 17892//#region src/webgl/color_mode.ts 17893const ZERO = 0; 17894const ONE = 1; 17895const ONE_MINUS_SRC_ALPHA = 771; 17896var ColorMode = class { 17897 constructor(blendFunction, blendColor, mask) { 17898 this.blendFunction = blendFunction; 17899 this.blendColor = blendColor; 17900 this.mask = mask; 17901 } 17902}; 17903ColorMode.Replace = [ONE, ZERO]; 17904ColorMode.disabled = new ColorMode(ColorMode.Replace, Color.transparent, [ 17905 false, 17906 false, 17907 false, 17908 false 17909]); 17910ColorMode.unblended = new ColorMode(ColorMode.Replace, Color.transparent, [ 17911 true, 17912 true, 17913 true, 17914 true 17915]); 17916ColorMode.alphaBlended = new ColorMode([ONE, ONE_MINUS_SRC_ALPHA], Color.transparent, [ 17917 true, 17918 true, 17919 true, 17920 true 17921]); 17922//#endregion 17923//#region src/webgl/context.ts 17924/** 17925* @internal 17926* A webgl wrapper class to allow injection, mocking and abstraction 17927*/ 17928var Context = class { 17929 constructor(gl) { 17930 this.gl = gl; 17931 this.clearColor = new ClearColor(this); 17932 this.clearDepth = new ClearDepth(this); 17933 this.clearStencil = new ClearStencil(this); 17934 this.colorMask = new ColorMask(this); 17935 this.depthMask = new DepthMask(this); 17936 this.stencilMask = new StencilMask(this); 17937 this.stencilFunc = new StencilFunc(this); 17938 this.stencilOp = new StencilOp(this); 17939 this.stencilTest = new StencilTest(this); 17940 this.depthRange = new DepthRange(this); 17941 this.depthTest = new DepthTest(this); 17942 this.depthFunc = new DepthFunc(this); 17943 this.blend = new Blend(this); 17944 this.blendFunc = new BlendFunc(this); 17945 this.blendColor = new BlendColor(this); 17946 this.blendEquation = new BlendEquation(this); 17947 this.cullFace = new CullFace(this); 17948 this.cullFaceSide = new CullFaceSide(this); 17949 this.frontFace = new FrontFace(this); 17950 this.program = new ProgramValue(this); 17951 this.activeTexture = new ActiveTextureUnit(this); 17952 this.viewport = new Viewport(this); 17953 this.bindFramebuffer = new BindFramebuffer(this); 17954 this.bindRenderbuffer = new BindRenderbuffer(this); 17955 this.bindTexture = new BindTexture(this); 17956 this.bindVertexBuffer = new BindVertexBuffer(this); 17957 this.bindElementBuffer = new BindElementBuffer(this); 17958 this.bindVertexArray = new BindVertexArray(this); 17959 this.pixelStoreUnpack = new PixelStoreUnpack(this); 17960 this.pixelStoreUnpackPremultiplyAlpha = new PixelStoreUnpackPremultiplyAlpha(this); 17961 this.pixelStoreUnpackFlipY = new PixelStoreUnpackFlipY(this); 17962 this.extTextureFilterAnisotropic = gl.getExtension("EXT_texture_filter_anisotropic"); 17963 if (this.extTextureFilterAnisotropic) this.extTextureFilterAnisotropicMax = gl.getParameter(this.extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT); 17964 this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); 17965 gl.getExtension("EXT_color_buffer_half_float"); 17966 gl.getExtension("EXT_color_buffer_float"); 17967 this.projectionUniformBuffer = createProjectionUniformBuffer(this); 17968 this.terrainUniformBuffer = createTerrainUniformBuffer(this); 17969 this.frameUniformBuffer = createFrameUniformBuffer(this); 17970 } 17971 setDefault() { 17972 this.unbindVAO(); 17973 this.clearColor.setDefault(); 17974 this.clearDepth.setDefault(); 17975 this.clearStencil.setDefault(); 17976 this.colorMask.setDefault(); 17977 this.depthMask.setDefault(); 17978 this.stencilMask.setDefault(); 17979 this.stencilFunc.setDefault(); 17980 this.stencilOp.setDefault(); 17981 this.stencilTest.setDefault(); 17982 this.depthRange.setDefault(); 17983 this.depthTest.setDefault(); 17984 this.depthFunc.setDefault(); 17985 this.blend.setDefault(); 17986 this.blendFunc.setDefault(); 17987 this.blendColor.setDefault(); 17988 this.blendEquation.setDefault(); 17989 this.cullFace.setDefault(); 17990 this.cullFaceSide.setDefault(); 17991 this.frontFace.setDefault(); 17992 this.program.setDefault(); 17993 this.activeTexture.setDefault(); 17994 this.bindFramebuffer.setDefault(); 17995 this.pixelStoreUnpack.setDefault(); 17996 this.pixelStoreUnpackPremultiplyAlpha.setDefault(); 17997 this.pixelStoreUnpackFlipY.setDefault(); 17998 } 17999 setDirty() { 18000 this.clearColor.dirty = true; 18001 this.clearDepth.dirty = true; 18002 this.clearStencil.dirty = true; 18003 this.colorMask.dirty = true; 18004 this.depthMask.dirty = true; 18005 this.stencilMask.dirty = true; 18006 this.stencilFunc.dirty = true; 18007 this.stencilOp.dirty = true; 18008 this.stencilTest.dirty = true; 18009 this.depthRange.dirty = true; 18010 this.depthTest.dirty = true; 18011 this.depthFunc.dirty = true; 18012 this.blend.dirty = true; 18013 this.blendFunc.dirty = true; 18014 this.blendColor.dirty = true; 18015 this.blendEquation.dirty = true; 18016 this.cullFace.dirty = true; 18017 this.cullFaceSide.dirty = true; 18018 this.frontFace.dirty = true; 18019 this.program.dirty = true; 18020 this.activeTexture.dirty = true; 18021 this.viewport.dirty = true; 18022 this.bindFramebuffer.dirty = true; 18023 this.bindRenderbuffer.dirty = true; 18024 this.bindTexture.dirty = true; 18025 this.bindVertexBuffer.dirty = true; 18026 this.bindElementBuffer.dirty = true; 18027 this.bindVertexArray.dirty = true; 18028 this.pixelStoreUnpack.dirty = true; 18029 this.pixelStoreUnpackPremultiplyAlpha.dirty = true; 18030 this.pixelStoreUnpackFlipY.dirty = true; 18031 this.projectionUniformBuffer.bindingDirty = true; 18032 this.terrainUniformBuffer.bindingDirty = true; 18033 this.frameUniformBuffer.bindingDirty = true; 18034 } 18035 /** 18036 * Reset some GL state to default values before handing users the raw context, as we do for 18037 * custom layers and WebGL style images, to avoid hard-to-debug bugs in their code. 18038 * 18039 * MapLibre restores all of its own state afterwards, so the only state worth resetting first 18040 * is state users would be surprised to find dirty: `CULL_FACE`, `TEXTURE0` and the three 18041 * `UNPACK_` settings, whose defaults are meaningful enough that most code assumes them. 18042 * The vertex array is unbound rather than reset, so that MapLibre never has to track it and 18043 * a user's `vertexAttribPointer` calls cannot land on one of ours. 18044 */ 18045 setCustomLayerDefaults() { 18046 this.unbindVAO(); 18047 this.cullFace.setDefault(); 18048 this.activeTexture.setDefault(); 18049 this.pixelStoreUnpack.setDefault(); 18050 this.pixelStoreUnpackPremultiplyAlpha.setDefault(); 18051 this.pixelStoreUnpackFlipY.setDefault(); 18052 } 18053 createIndexBuffer(array, dynamicDraw) { 18054 return new IndexBuffer(this, array, dynamicDraw); 18055 } 18056 createVertexBuffer(array, attributes, dynamicDraw) { 18057 return new VertexBuffer(this, array, attributes, dynamicDraw); 18058 } 18059 createRenderbuffer(storageFormat, width, height) { 18060 const gl = this.gl; 18061 const rbo = gl.createRenderbuffer(); 18062 this.bindRenderbuffer.set(rbo); 18063 gl.renderbufferStorage(gl.RENDERBUFFER, storageFormat, width, height); 18064 this.bindRenderbuffer.set(null); 18065 return rbo; 18066 } 18067 createFramebuffer(width, height, hasDepth, hasStencil) { 18068 return new Framebuffer(this, width, height, hasDepth, hasStencil); 18069 } 18070 clear({ color, depth, stencil }) { 18071 const gl = this.gl; 18072 let mask = 0; 18073 if (color) { 18074 mask |= gl.COLOR_BUFFER_BIT; 18075 this.clearColor.set(color); 18076 this.colorMask.set([ 18077 true, 18078 true, 18079 true, 18080 true 18081 ]); 18082 } 18083 if (typeof depth !== "undefined") { 18084 mask |= gl.DEPTH_BUFFER_BIT; 18085 this.depthRange.set([0, 1]); 18086 this.clearDepth.set(depth); 18087 this.depthMask.set(true); 18088 } 18089 if (typeof stencil !== "undefined") { 18090 mask |= gl.STENCIL_BUFFER_BIT; 18091 this.clearStencil.set(stencil); 18092 this.stencilMask.set(255); 18093 } 18094 gl.clear(mask); 18095 } 18096 setCullFace(cullFaceMode) { 18097 if (cullFaceMode.enable === false) this.cullFace.set(false); 18098 else { 18099 this.cullFace.set(true); 18100 this.cullFaceSide.set(cullFaceMode.mode); 18101 this.frontFace.set(cullFaceMode.frontFace); 18102 } 18103 } 18104 setDepthMode(depthMode) { 18105 if (depthMode.func === this.gl.ALWAYS && !depthMode.mask) this.depthTest.set(false); 18106 else { 18107 this.depthTest.set(true); 18108 this.depthFunc.set(depthMode.func); 18109 this.depthMask.set(depthMode.mask); 18110 this.depthRange.set(depthMode.range); 18111 } 18112 } 18113 setStencilMode(stencilMode) { 18114 if (stencilMode.test.func === this.gl.ALWAYS && !stencilMode.mask) this.stencilTest.set(false); 18115 else { 18116 this.stencilTest.set(true); 18117 this.stencilMask.set(stencilMode.mask); 18118 this.stencilOp.set([ 18119 stencilMode.fail, 18120 stencilMode.depthFail, 18121 stencilMode.pass 18122 ]); 18123 this.stencilFunc.set({ 18124 func: stencilMode.test.func, 18125 ref: stencilMode.ref, 18126 mask: stencilMode.test.mask 18127 }); 18128 } 18129 } 18130 setColorMode(colorMode) { 18131 if (deepEqual(colorMode.blendFunction, ColorMode.Replace)) this.blend.set(false); 18132 else { 18133 this.blend.set(true); 18134 this.blendFunc.set(colorMode.blendFunction); 18135 this.blendColor.set(colorMode.blendColor); 18136 } 18137 this.colorMask.set(colorMode.mask); 18138 } 18139 createVertexArray() { 18140 return this.gl.createVertexArray(); 18141 } 18142 deleteVertexArray(x) { 18143 this.gl.deleteVertexArray(x); 18144 } 18145 unbindVAO() { 18146 this.bindVertexArray.set(null); 18147 } 18148}; 18149//#endregion 18150//#region src/webgl/depth_mode.ts 18151const ALWAYS$1 = 519; 18152var DepthMode = class { 18153 constructor(depthFunc, depthMask, depthRange) { 18154 this.func = depthFunc; 18155 this.mask = depthMask; 18156 this.range = depthRange; 18157 } 18158}; 18159DepthMode.ReadOnly = false; 18160DepthMode.ReadWrite = true; 18161DepthMode.disabled = new DepthMode(ALWAYS$1, DepthMode.ReadOnly, [0, 1]); 18162//#endregion 18163//#region src/webgl/stencil_mode.ts 18164const ALWAYS = 519; 18165const KEEP = 7680; 18166var StencilMode = class { 18167 constructor(test, ref, mask, fail, depthFail, pass) { 18168 this.test = test; 18169 this.ref = ref; 18170 this.mask = mask; 18171 this.fail = fail; 18172 this.depthFail = depthFail; 18173 this.pass = pass; 18174 } 18175}; 18176StencilMode.disabled = new StencilMode({ 18177 func: ALWAYS, 18178 mask: 0 18179}, 0, 0, KEEP, KEEP, KEEP); 18180//#endregion 18181//#region src/webgl/cull_face_mode.ts 18182const FRONT = 1028; 18183const BACK = 1029; 18184const CCW = 2305; 18185var CullFaceMode = class { 18186 constructor(enable, mode, frontFace) { 18187 this.enable = enable; 18188 this.mode = mode; 18189 this.frontFace = frontFace; 18190 } 18191}; 18192CullFaceMode.disabled = new CullFaceMode(false, BACK, CCW); 18193CullFaceMode.backCCW = new CullFaceMode(true, BACK, CCW); 18194CullFaceMode.frontCCW = new CullFaceMode(true, FRONT, CCW); 18195//#endregion 18196//#region src/render/render_context.ts 18197function createRenderContext(transform, projection, terrain) { 18198 const projectionTransition = projection?.transitionState ?? 0; 18199 return { 18200 currentPass: "offscreen", 18201 currentLayer: 0, 18202 opaquePassCutoff: Infinity, 18203 depthRangeFor3D: [0, 1], 18204 isRenderingToTexture: false, 18205 transform, 18206 terrain, 18207 projectionTransition, 18208 isRenderingGlobe: projectionTransition > 0 18209 }; 18210} 18211function getProjectionDataForTile(renderContext, tileID, options = {}) { 18212 return renderContext.transform.getProjectionData({ 18213 overscaledTileID: tileID, 18214 aligned: options.aligned, 18215 applyGlobeMatrix: !renderContext.isRenderingToTexture, 18216 applyTerrainMatrix: options.applyTerrainMatrix ?? true 18217 }); 18218} 18219/** 18220* Returns terrain data for a tile. 18221* Returns null if terrain is not configured or tiles are being rendered to a texture. 18222*/ 18223function getTerrainDataForTile(renderContext, tileID) { 18224 if (renderContext.isRenderingToTexture) return null; 18225 return renderContext.terrain?.getTerrainData(tileID) ?? null; 18226} 18227//#endregion 18228//#region src/webgl/draw/draw_collision_debug.ts 18229let quadTriangles; 18230function drawCollisionDebug(painter, tileManager, layer, coords, isText, renderContext) { 18231 const context = painter.context; 18232 const gl = context.gl; 18233 const program = painter.useProgram("collisionBox"); 18234 const tileBatches = []; 18235 let circleCount = 0; 18236 let circleOffset = 0; 18237 for (const coord of coords) { 18238 const bucket = tileManager.getTile(coord).getBucket(layer); 18239 if (!bucket) continue; 18240 const buffers = isText ? bucket.textCollisionBox : bucket.iconCollisionBox; 18241 const circleArray = bucket.collisionCircleArray; 18242 if (circleArray.length > 0) { 18243 tileBatches.push({ 18244 circleArray, 18245 circleOffset, 18246 coord 18247 }); 18248 circleCount += circleArray.length / 4; 18249 circleOffset = circleCount; 18250 } 18251 if (!buffers) continue; 18252 program.draw(context, gl.LINES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, null, getTerrainDataForTile(renderContext, coord), getProjectionDataForTile(renderContext, coord), layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer, buffers.segments, null, painter.transform.zoom, null, null, buffers.collisionVertexBuffer); 18253 } 18254 if (!isText || !tileBatches.length) return; 18255 const circleProgram = painter.useProgram("collisionCircle"); 18256 const vertexData = new CollisionCircleLayoutArray(); 18257 vertexData.resize(circleCount * 4); 18258 vertexData._trim(); 18259 let vertexOffset = 0; 18260 for (const batch of tileBatches) for (let i = 0; i < batch.circleArray.length / 4; i++) { 18261 const circleIdx = i * 4; 18262 const x = batch.circleArray[circleIdx + 0]; 18263 const y = batch.circleArray[circleIdx + 1]; 18264 const radius = batch.circleArray[circleIdx + 2]; 18265 const collision = batch.circleArray[circleIdx + 3]; 18266 vertexData.emplace(vertexOffset++, x, y, radius, collision, 0); 18267 vertexData.emplace(vertexOffset++, x, y, radius, collision, 1); 18268 vertexData.emplace(vertexOffset++, x, y, radius, collision, 2); 18269 vertexData.emplace(vertexOffset++, x, y, radius, collision, 3); 18270 } 18271 if (!quadTriangles || quadTriangles.length < circleCount * 2) quadTriangles = createQuadTriangles(circleCount); 18272 const indexBuffer = context.createIndexBuffer(quadTriangles, true); 18273 const vertexBuffer = context.createVertexBuffer(vertexData, collisionCircleLayout.members, true); 18274 for (const batch of tileBatches) circleProgram.draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, null, getTerrainDataForTile(renderContext, batch.coord), null, layer.id, vertexBuffer, indexBuffer, SegmentVector.simpleSegment(0, batch.circleOffset * 2, batch.circleArray.length, batch.circleArray.length / 2), null, painter.transform.zoom, null, null, null); 18275 vertexBuffer.destroy(); 18276 indexBuffer.destroy(); 18277} 18278function createQuadTriangles(quadCount) { 18279 const triCount = quadCount * 2; 18280 const array = new QuadTriangleArray(); 18281 array.resize(triCount); 18282 array._trim(); 18283 for (let i = 0; i < triCount; i++) { 18284 const idx = i * 6; 18285 array.uint16[idx + 0] = i * 4 + 0; 18286 array.uint16[idx + 1] = i * 4 + 1; 18287 array.uint16[idx + 2] = i * 4 + 2; 18288 array.uint16[idx + 3] = i * 4 + 2; 18289 array.uint16[idx + 4] = i * 4 + 3; 18290 array.uint16[idx + 5] = i * 4 + 0; 18291 } 18292 return array; 18293} 18294//#endregion 18295//#region src/webgl/draw/draw_symbol.ts 18296const identityMat4 = identity(/* @__PURE__ */ new Float32Array(16)); 18297function drawSymbols(painter, tileManager, layer, coords, variableOffsets, renderContext) { 18298 if (renderContext.currentPass !== "translucent") return; 18299 const stencilMode = StencilMode.disabled; 18300 const colorMode = painter.colorModeForRenderPass(); 18301 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); 18302 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, renderContext); 18303 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, renderContext); 18304 if (tileManager.map.showCollisionBoxes) { 18305 drawCollisionDebug(painter, tileManager, layer, coords, true, renderContext); 18306 drawCollisionDebug(painter, tileManager, layer, coords, false, renderContext); 18307 } 18308} 18309function calculateVariableRenderShift(anchor, width, height, textOffset, textBoxScale, renderTextSize) { 18310 const { horizontalAlign, verticalAlign } = getAnchorAlignment(anchor); 18311 const shiftX = -(horizontalAlign - .5) * width; 18312 const shiftY = -(verticalAlign - .5) * height; 18313 return new Point((shiftX / textBoxScale + textOffset[0]) * renderTextSize, (shiftY / textBoxScale + textOffset[1]) * renderTextSize); 18314} 18315function updateVariableAnchors(coords, painter, layer, tileManager, rotationAlignment, pitchAlignment, translate, translateAnchor, variableOffsets) { 18316 const transform = painter.transform; 18317 const terrain = painter.style.map.terrain; 18318 const rotateWithMap = rotationAlignment === "map"; 18319 const pitchWithMap = pitchAlignment === "map"; 18320 for (const coord of coords) { 18321 const tile = tileManager.getTile(coord); 18322 const bucket = tile.getBucket(layer); 18323 if (!bucket?.text?.segments.get().length) continue; 18324 const sizeData = bucket.textSizeData; 18325 const size = evaluateSizeForZoom(sizeData, transform.zoom); 18326 const pixelToTileScale = pixelsToTileUnits(tile, 1, painter.transform.zoom); 18327 const pitchedLabelPlaneMatrix = getPitchedLabelPlaneMatrix(rotateWithMap, painter.transform, pixelToTileScale); 18328 const updateTextFitIcon = layer.layout.get("icon-text-fit") !== "none" && bucket.hasIconData(); 18329 if (size) { 18330 const tileScale = Math.pow(2, transform.zoom - tile.tileID.overscaledZ); 18331 const getElevation = terrain ? (x, y) => terrain.getElevation(coord, x, y) : void 0; 18332 updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, variableOffsets, transform, pitchedLabelPlaneMatrix, tileScale, size, updateTextFitIcon, translatePosition(transform, tile, translate, translateAnchor), coord.toUnwrapped(), getElevation, layer.layout.get("symbol-height-anchor") === "ground"); 18333 } 18334 } 18335} 18336function getShiftedAnchor(projectedAnchorPoint, projectionContext, rotateWithMap, shift, transformAngle, pitchedTextShiftCorrection) { 18337 const translatedAnchor = projectionContext.tileAnchorPoint.add(new Point(projectionContext.translation[0], projectionContext.translation[1])); 18338 if (projectionContext.pitchWithMap) { 18339 let adjustedShift = shift.mult(pitchedTextShiftCorrection); 18340 if (!rotateWithMap) adjustedShift = adjustedShift.rotate(-transformAngle); 18341 const tileAnchorShifted = translatedAnchor.add(adjustedShift); 18342 return projectWithMatrix(tileAnchorShifted.x, tileAnchorShifted.y, projectionContext.pitchedLabelPlaneMatrix, elevationAt(projectionContext, tileAnchorShifted.x, tileAnchorShifted.y)).point; 18343 } else if (rotateWithMap) { 18344 const east = projectTileCoordinatesToLabelPlane(projectionContext.tileAnchorPoint.x + 1, projectionContext.tileAnchorPoint.y, projectionContext).point.sub(projectedAnchorPoint); 18345 const angle = Math.atan(east.y / east.x) + (east.x < 0 ? Math.PI : 0); 18346 return projectedAnchorPoint.add(shift.rotate(angle)); 18347 } else return projectedAnchorPoint.add(shift); 18348} 18349function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, variableOffsets, transform, pitchedLabelPlaneMatrix, tileScale, size, updateTextFitIcon, translation, unwrappedTileID, getElevation, heightAnchorGround) { 18350 const placedSymbols = bucket.text.placedSymbolArray; 18351 const dynamicTextLayoutVertexArray = bucket.text.dynamicLayoutVertexArray; 18352 const dynamicIconLayoutVertexArray = bucket.icon.dynamicLayoutVertexArray; 18353 const placedTextShifts = {}; 18354 dynamicTextLayoutVertexArray.clear(); 18355 for (let s = 0; s < placedSymbols.length; s++) { 18356 const symbol = placedSymbols.get(s); 18357 const skipOrientation = bucket.allowVerticalPlacement && !symbol.placedOrientation; 18358 const variableOffset = !symbol.hidden && symbol.crossTileID && !skipOrientation ? variableOffsets[symbol.crossTileID] : null; 18359 if (!variableOffset) hideGlyphs(symbol.numGlyphs, dynamicTextLayoutVertexArray); 18360 else { 18361 const tileAnchor = new Point(symbol.anchorX, symbol.anchorY); 18362 const projectionContext = { 18363 getElevation, 18364 heightOffset: symbol.heightOffset, 18365 heightAnchorGround, 18366 width: transform.width, 18367 height: transform.height, 18368 pitchedLabelPlaneMatrix, 18369 lineVertexArray: null, 18370 pitchWithMap, 18371 transform, 18372 projectionCache: null, 18373 tileAnchorPoint: tileAnchor, 18374 translation, 18375 unwrappedTileID 18376 }; 18377 const projectedAnchor = pitchWithMap ? projectTileCoordinatesToClipSpace(tileAnchor.x, tileAnchor.y, projectionContext) : projectTileCoordinatesToLabelPlane(tileAnchor.x, tileAnchor.y, projectionContext); 18378 const perspectiveRatio = getPerspectiveRatio(transform.cameraToCenterDistance, projectedAnchor.signedDistanceFromCamera); 18379 let renderTextSize = evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / 24; 18380 if (pitchWithMap) renderTextSize *= bucket.tilePixelRatio / tileScale; 18381 const { width, height, anchor, textOffset, textBoxScale } = variableOffset; 18382 const shift = calculateVariableRenderShift(anchor, width, height, textOffset, textBoxScale, renderTextSize); 18383 const pitchedTextCorrection = transform.getPitchedTextCorrection(tileAnchor.x + translation[0], tileAnchor.y + translation[1], unwrappedTileID); 18384 const shiftedAnchor = getShiftedAnchor(projectedAnchor.point, projectionContext, rotateWithMap, shift, -transform.bearingInRadians, pitchedTextCorrection); 18385 const angle = bucket.allowVerticalPlacement && symbol.placedOrientation === 2 ? Math.PI / 2 : 0; 18386 for (let g = 0; g < symbol.numGlyphs; g++) addDynamicAttributes(dynamicTextLayoutVertexArray, shiftedAnchor, angle); 18387 if (updateTextFitIcon && symbol.associatedIconIndex >= 0) placedTextShifts[symbol.associatedIconIndex] = { 18388 shiftedAnchor, 18389 angle 18390 }; 18391 } 18392 } 18393 if (updateTextFitIcon) { 18394 dynamicIconLayoutVertexArray.clear(); 18395 const placedIcons = bucket.icon.placedSymbolArray; 18396 for (let i = 0; i < placedIcons.length; i++) { 18397 const placedIcon = placedIcons.get(i); 18398 if (placedIcon.hidden) hideGlyphs(placedIcon.numGlyphs, dynamicIconLayoutVertexArray); 18399 else { 18400 const shift = placedTextShifts[i]; 18401 if (!shift) hideGlyphs(placedIcon.numGlyphs, dynamicIconLayoutVertexArray); 18402 else for (let g = 0; g < placedIcon.numGlyphs; g++) addDynamicAttributes(dynamicIconLayoutVertexArray, shift.shiftedAnchor, shift.angle); 18403 } 18404 } 18405 bucket.icon.dynamicLayoutVertexBuffer.updateData(dynamicIconLayoutVertexArray); 18406 } 18407 bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicTextLayoutVertexArray); 18408} 18409function getSymbolProgramName(isSDF, isText, bucket) { 18410 if (bucket.iconsInText && isText) return "symbolTextAndIcon"; 18411 else if (isSDF) return "symbolSDF"; 18412 else return "symbolIcon"; 18413} 18414function drawLayerSymbols(painter, tileManager, layer, coords, isText, translate, translateAnchor, rotationAlignment, pitchAlignment, keepUpright, stencilMode, colorMode, renderContext) { 18415 const context = painter.context; 18416 const gl = context.gl; 18417 const transform = painter.transform; 18418 const rotateWithMap = rotationAlignment === "map"; 18419 const pitchWithMap = pitchAlignment === "map"; 18420 const alongLine = rotationAlignment !== "viewport" && layer.layout.get("symbol-placement") !== "point"; 18421 const rotateInShader = rotateWithMap && !pitchWithMap && !alongLine; 18422 const hasSortKey = !layer.layout.get("symbol-sort-key").isConstant(); 18423 let sortFeaturesByKey = false; 18424 const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 18425 const hasVariablePlacement = layer._unevaluatedLayout.hasValue("text-variable-anchor") || layer._unevaluatedLayout.hasValue("text-variable-anchor-offset"); 18426 const tileRenderState = []; 18427 const pitchedTextRescaling = transform.getCircleRadiusCorrection(); 18428 for (const coord of coords) { 18429 const tile = tileManager.getTile(coord); 18430 const bucket = tile.getBucket(layer); 18431 if (!bucket) continue; 18432 const buffers = isText ? bucket.text : bucket.icon; 18433 if (!buffers?.segments.get().length || !buffers.hasVisibleVertices) continue; 18434 const programConfiguration = buffers.programConfigurations.get(layer.id); 18435 const isSDF = isText || bucket.sdfIcons; 18436 const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; 18437 const transformed = pitchWithMap || transform.pitch !== 0; 18438 const program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration); 18439 const size = evaluateSizeForZoom(sizeData, transform.zoom); 18440 const terrainData = getTerrainDataForTile(renderContext, coord); 18441 let texSize; 18442 let texSizeIcon = [0, 0]; 18443 let atlasTexture; 18444 let atlasInterpolation; 18445 let atlasTextureIcon = null; 18446 let atlasInterpolationIcon; 18447 if (isText) { 18448 atlasTexture = tile.glyphAtlasTexture; 18449 atlasInterpolation = gl.LINEAR; 18450 texSize = tile.glyphAtlasTexture.size; 18451 if (bucket.iconsInText) { 18452 texSizeIcon = tile.imageAtlasTexture.size; 18453 atlasTextureIcon = tile.imageAtlasTexture; 18454 const zoomDependentSize = sizeData.kind === "composite" || sizeData.kind === "camera"; 18455 atlasInterpolationIcon = transformed || painter.options.rotating || painter.options.zooming || zoomDependentSize ? gl.LINEAR : gl.NEAREST; 18456 } 18457 } else { 18458 const iconScaled = layer.layout.get("icon-size").constantOr(0) !== 1 || bucket.iconsNeedLinear; 18459 atlasTexture = tile.imageAtlasTexture; 18460 atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || transformed ? gl.LINEAR : gl.NEAREST; 18461 texSize = tile.imageAtlasTexture.size; 18462 } 18463 const s = pixelsToTileUnits(tile, 1, painter.transform.zoom); 18464 const pitchedLabelPlaneMatrix = getPitchedLabelPlaneMatrix(rotateWithMap, painter.transform, s); 18465 const glCoordMatrixForShader = getGlCoordMatrix(pitchWithMap, rotateWithMap, painter.transform, s); 18466 const translation = translatePosition(transform, tile, translate, translateAnchor); 18467 const projectionData = getProjectionDataForTile(renderContext, coord); 18468 const hasVariableAnchors = hasVariablePlacement && bucket.hasTextData(); 18469 const updateTextFitIcon = layer.layout.get("icon-text-fit") !== "none" && hasVariableAnchors && bucket.hasIconData(); 18470 const isOffset = layer._unevaluatedLayout.hasValue("icon-offset"); 18471 const heightAnchorGround = layer.layout.get("symbol-height-anchor") === "ground"; 18472 if (alongLine) { 18473 const pitchedLabelPlaneMatrixInverse = create$1(); 18474 fastInvertTransformMat4(pitchedLabelPlaneMatrixInverse, pitchedLabelPlaneMatrix); 18475 const getElevation = painter.style.map.terrain ? (x, y) => painter.style.map.terrain.getElevation(coord, x, y) : void 0; 18476 updateLineLabels(bucket, painter, isText, pitchedLabelPlaneMatrix, pitchedLabelPlaneMatrixInverse, pitchWithMap, keepUpright, layer.layout.get("text-rotation-alignment") === "map", coord.toUnwrapped(), transform.width, transform.height, translation, getElevation); 18477 } 18478 const shaderVariableAnchor = isText && hasVariablePlacement || updateTextFitIcon; 18479 const combinedLabelPlaneMatrix = pitchWithMap ? pitchedLabelPlaneMatrix : painter.transform.clipSpaceToPixelsMatrix; 18480 const uLabelPlaneMatrix = alongLine || shaderVariableAnchor ? identityMat4 : combinedLabelPlaneMatrix; 18481 const hasHalo = isSDF && layer.paint.get(isText ? "text-halo-width" : "icon-halo-width").constantOr(1) !== 0; 18482 let uniformValues; 18483 if (isSDF) { 18484 if (!bucket.iconsInText) uniformValues = symbolSDFUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, alongLine, shaderVariableAnchor, painter, uLabelPlaneMatrix, glCoordMatrixForShader, translation, isText, texSize, hasHalo, pitchedTextRescaling, isOffset, heightAnchorGround); 18485 else uniformValues = symbolTextAndIconUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, alongLine, shaderVariableAnchor, painter, uLabelPlaneMatrix, glCoordMatrixForShader, translation, texSize, texSizeIcon, pitchedTextRescaling, isOffset, heightAnchorGround); 18486 } else uniformValues = symbolIconUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, alongLine, shaderVariableAnchor, painter, uLabelPlaneMatrix, glCoordMatrixForShader, translation, isText, texSize, pitchedTextRescaling, isOffset, heightAnchorGround); 18487 const state = { 18488 program, 18489 buffers, 18490 uniformValues, 18491 projectionData, 18492 atlasTexture, 18493 atlasTextureIcon, 18494 atlasInterpolation, 18495 atlasInterpolationIcon, 18496 isSDF, 18497 hasHalo 18498 }; 18499 if (hasSortKey && bucket.canOverlap) { 18500 sortFeaturesByKey = true; 18501 const oldSegments = buffers.segments.get(); 18502 for (const segment of oldSegments) tileRenderState.push({ 18503 segments: new SegmentVector([segment]), 18504 sortKey: segment.sortKey, 18505 state, 18506 terrainData 18507 }); 18508 } else tileRenderState.push({ 18509 segments: buffers.segments, 18510 sortKey: 0, 18511 state, 18512 terrainData 18513 }); 18514 } 18515 if (sortFeaturesByKey) tileRenderState.sort((a, b) => a.sortKey - b.sortKey); 18516 const haloWidth = layer.paint.get(isText ? "text-halo-width" : "icon-halo-width").constantOr(null) ?? Infinity; 18517 const isGlyphOverlap = layer.layout.get("text-letter-spacing").constantOr(0) * 24 < 0 || haloWidth > 1; 18518 for (const segmentState of tileRenderState) { 18519 const state = segmentState.state; 18520 context.activeTexture.set(gl.TEXTURE0); 18521 state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); 18522 if (state.atlasTextureIcon) { 18523 context.activeTexture.set(gl.TEXTURE1); 18524 if (state.atlasTextureIcon) state.atlasTextureIcon.bind(state.atlasInterpolationIcon, gl.CLAMP_TO_EDGE); 18525 } 18526 const isHalo = state.isSDF && state.hasHalo; 18527 if (isHalo) { 18528 const uniformValues = state.uniformValues; 18529 uniformValues["u_is_halo"] = 1; 18530 if (isGlyphOverlap) { 18531 uniformValues["u_is_plain"] = 0; 18532 drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, uniformValues, state.projectionData, segmentState.terrainData); 18533 uniformValues["u_is_halo"] = 0; 18534 uniformValues["u_is_plain"] = 1; 18535 } 18536 } 18537 drawSymbolElements(state.buffers, segmentState.segments, layer, painter, state.program, depthMode, stencilMode, colorMode, state.uniformValues, state.projectionData, segmentState.terrainData); 18538 if (isHalo && !isGlyphOverlap) state.uniformValues["u_is_halo"] = 0; 18539 } 18540} 18541function drawSymbolElements(buffers, segments, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues, projectionData, terrainData) { 18542 const context = painter.context; 18543 const gl = context.gl; 18544 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer, segments, layer.paint, painter.transform.zoom, buffers.programConfigurations.get(layer.id), buffers.dynamicLayoutVertexBuffer, buffers.opacityVertexBuffer); 18545} 18546//#endregion 18547//#region src/webgl/draw/draw_circle.ts 18548function drawCircles(painter, tileManager, layer, coords, renderContext) { 18549 if (renderContext.currentPass !== "translucent") return; 18550 const opacity = layer.paint.get("circle-opacity"); 18551 const strokeWidth = layer.paint.get("circle-stroke-width"); 18552 const strokeOpacity = layer.paint.get("circle-stroke-opacity"); 18553 const sortFeaturesByKey = !layer.layout.get("circle-sort-key").isConstant(); 18554 if (opacity.constantOr(1) === 0 && (strokeWidth.constantOr(1) === 0 || strokeOpacity.constantOr(1) === 0)) return; 18555 const context = painter.context; 18556 const gl = context.gl; 18557 const transform = painter.transform; 18558 const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 18559 const stencilMode = StencilMode.disabled; 18560 const colorMode = painter.colorModeForRenderPass(); 18561 const segmentsRenderStates = []; 18562 const radiusCorrectionFactor = transform.getCircleRadiusCorrection(); 18563 for (const coord of coords) { 18564 const tile = tileManager.getTile(coord); 18565 const bucket = tile.getBucket(layer); 18566 if (!bucket) continue; 18567 const styleTranslate = layer.paint.get("circle-translate"); 18568 const styleTranslateAnchor = layer.paint.get("circle-translate-anchor"); 18569 const translateForUniforms = translatePosition(transform, tile, styleTranslate, styleTranslateAnchor); 18570 const programConfiguration = bucket.programConfigurations.get(layer.id); 18571 const program = painter.useProgram("circle", programConfiguration); 18572 const layoutVertexBuffer = bucket.layoutVertexBuffer; 18573 const indexBuffer = bucket.indexBuffer; 18574 const terrainData = getTerrainDataForTile(renderContext, coord); 18575 const state = { 18576 programConfiguration, 18577 program, 18578 layoutVertexBuffer, 18579 indexBuffer, 18580 uniformValues: circleUniformValues(painter, tile, layer, translateForUniforms, radiusCorrectionFactor), 18581 terrainData, 18582 projectionData: getProjectionDataForTile(renderContext, coord) 18583 }; 18584 if (sortFeaturesByKey) { 18585 const oldSegments = bucket.segments.get(); 18586 for (const segment of oldSegments) segmentsRenderStates.push({ 18587 segments: new SegmentVector([segment]), 18588 sortKey: segment.sortKey, 18589 state 18590 }); 18591 } else segmentsRenderStates.push({ 18592 segments: bucket.segments, 18593 sortKey: 0, 18594 state 18595 }); 18596 } 18597 if (sortFeaturesByKey) segmentsRenderStates.sort((a, b) => a.sortKey - b.sortKey); 18598 for (const segmentsState of segmentsRenderStates) { 18599 const { programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues, terrainData, projectionData } = segmentsState.state; 18600 const segments = segmentsState.segments; 18601 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, layoutVertexBuffer, indexBuffer, segments, layer.paint, painter.transform.zoom, programConfiguration); 18602 } 18603} 18604//#endregion 18605//#region src/webgl/draw/draw_heatmap.ts 18606function drawHeatmap(painter, tileManager, layer, tileIDs, renderContext) { 18607 if (layer.paint.get("heatmap-opacity") === 0) return; 18608 const context = painter.context; 18609 if (painter.style.map.terrain) { 18610 for (const coord of tileIDs) { 18611 const tile = tileManager.getTile(coord); 18612 if (tileManager.hasRenderableParent(coord)) continue; 18613 if (renderContext.currentPass === "offscreen") prepareHeatmapTerrain(painter, tile, layer, coord, renderContext); 18614 else if (renderContext.currentPass === "translucent") renderHeatmapTerrain(painter, layer, coord, renderContext); 18615 } 18616 context.viewport.set([ 18617 0, 18618 0, 18619 painter.width, 18620 painter.height 18621 ]); 18622 } else if (renderContext.currentPass === "offscreen") prepareHeatmapFlat(painter, tileManager, layer, tileIDs, renderContext); 18623 else if (renderContext.currentPass === "translucent") renderHeatmapFlat(painter, layer); 18624} 18625function prepareHeatmapFlat(painter, tileManager, layer, coords, renderContext) { 18626 const context = painter.context; 18627 const gl = context.gl; 18628 const transform = painter.transform; 18629 const stencilMode = StencilMode.disabled; 18630 const colorMode = new ColorMode([gl.ONE, gl.ONE], Color.transparent, [ 18631 true, 18632 true, 18633 true, 18634 true 18635 ]); 18636 bindFramebuffer(context, painter, layer); 18637 context.clear({ color: Color.transparent }); 18638 for (const coord of coords) { 18639 if (tileManager.hasRenderableParent(coord)) continue; 18640 const tile = tileManager.getTile(coord); 18641 const bucket = tile.getBucket(layer); 18642 if (!bucket) continue; 18643 const programConfiguration = bucket.programConfigurations.get(layer.id); 18644 const program = painter.useProgram("heatmap", programConfiguration); 18645 const projectionData = getProjectionDataForTile(renderContext, coord, { applyTerrainMatrix: false }); 18646 const radiusCorrectionFactor = transform.getCircleRadiusCorrection(); 18647 program.draw(context, gl.TRIANGLES, DepthMode.disabled, stencilMode, colorMode, CullFaceMode.backCCW, heatmapUniformValues(tile, transform.zoom, layer.paint.get("heatmap-intensity"), radiusCorrectionFactor), null, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, transform.zoom, programConfiguration); 18648 } 18649 context.viewport.set([ 18650 0, 18651 0, 18652 painter.width, 18653 painter.height 18654 ]); 18655} 18656function renderHeatmapFlat(painter, layer) { 18657 const context = painter.context; 18658 const gl = context.gl; 18659 context.setColorMode(painter.colorModeForRenderPass()); 18660 const fbo = layer.heatmapFbos.get(HEATMAP_FULL_RENDER_FBO_KEY); 18661 if (!fbo) return; 18662 context.activeTexture.set(gl.TEXTURE0); 18663 gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); 18664 context.activeTexture.set(gl.TEXTURE1); 18665 getColorRampTexture(context, layer).bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 18666 painter.useProgram("heatmapTexture").draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, heatmapTextureUniformValues(painter, layer, 0, 1), null, null, layer.id, painter.viewportBuffer, painter.quadTriangleIndexBuffer, painter.viewportSegments, layer.paint, painter.transform.zoom); 18667} 18668function prepareHeatmapTerrain(painter, tile, layer, coord, renderContext) { 18669 const context = painter.context; 18670 const gl = context.gl; 18671 const stencilMode = StencilMode.disabled; 18672 const colorMode = new ColorMode([gl.ONE, gl.ONE], Color.transparent, [ 18673 true, 18674 true, 18675 true, 18676 true 18677 ]); 18678 const bucket = tile.getBucket(layer); 18679 if (!bucket) return; 18680 const tileKey = coord.key; 18681 let fbo = layer.heatmapFbos.get(tileKey); 18682 if (!fbo) { 18683 fbo = createHeatmapFbo(context, tile.tileSize, tile.tileSize); 18684 layer.heatmapFbos.set(tileKey, fbo); 18685 } 18686 context.bindFramebuffer.set(fbo.framebuffer); 18687 context.viewport.set([ 18688 0, 18689 0, 18690 tile.tileSize, 18691 tile.tileSize 18692 ]); 18693 context.clear({ color: Color.transparent }); 18694 const programConfiguration = bucket.programConfigurations.get(layer.id); 18695 const program = painter.useProgram("heatmap", programConfiguration, !renderContext.isRenderingGlobe); 18696 const projectionData = getProjectionDataForTile(renderContext, tile.tileID); 18697 const terrainData = getTerrainDataForTile(renderContext, coord); 18698 program.draw(context, gl.TRIANGLES, DepthMode.disabled, stencilMode, colorMode, CullFaceMode.disabled, heatmapUniformValues(tile, painter.transform.zoom, layer.paint.get("heatmap-intensity"), 1), terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, painter.transform.zoom, programConfiguration); 18699} 18700function renderHeatmapTerrain(painter, layer, coord, renderContext) { 18701 const context = painter.context; 18702 const gl = context.gl; 18703 const transform = painter.transform; 18704 context.setColorMode(painter.colorModeForRenderPass()); 18705 const colorRampTexture = getColorRampTexture(context, layer); 18706 const tileKey = coord.key; 18707 const fbo = layer.heatmapFbos.get(tileKey); 18708 if (!fbo) return; 18709 context.activeTexture.set(gl.TEXTURE0); 18710 gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); 18711 context.activeTexture.set(gl.TEXTURE1); 18712 colorRampTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 18713 const projectionData = getProjectionDataForTile(renderContext, coord, { applyTerrainMatrix: renderContext.isRenderingGlobe }); 18714 painter.useProgram("heatmapTexture").draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, heatmapTextureUniformValues(painter, layer, 0, 1), null, projectionData, layer.id, painter.rasterBoundsBuffer, painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments, layer.paint, transform.zoom); 18715 fbo.destroy(); 18716 layer.heatmapFbos.delete(tileKey); 18717} 18718function bindFramebuffer(context, painter, layer) { 18719 const gl = context.gl; 18720 context.activeTexture.set(gl.TEXTURE1); 18721 context.viewport.set([ 18722 0, 18723 0, 18724 painter.width / 4, 18725 painter.height / 4 18726 ]); 18727 let fbo = layer.heatmapFbos.get(HEATMAP_FULL_RENDER_FBO_KEY); 18728 if (!fbo) { 18729 fbo = createHeatmapFbo(context, painter.width / 4, painter.height / 4); 18730 layer.heatmapFbos.set(HEATMAP_FULL_RENDER_FBO_KEY, fbo); 18731 } else { 18732 gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); 18733 context.bindFramebuffer.set(fbo.framebuffer); 18734 } 18735} 18736function createHeatmapFbo(context, width, height) { 18737 const gl = context.gl; 18738 const texture = gl.createTexture(); 18739 gl.bindTexture(gl.TEXTURE_2D, texture); 18740 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 18741 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 18742 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 18743 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 18744 gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA16F, width, height); 18745 const fbo = context.createFramebuffer(width, height, false, false); 18746 fbo.colorAttachment.set(texture); 18747 return fbo; 18748} 18749function getColorRampTexture(context, layer) { 18750 layer.colorRampTexture ||= new Texture(context, layer.colorRamp, context.gl.RGBA); 18751 return layer.colorRampTexture; 18752} 18753//#endregion 18754//#region src/webgl/draw/draw_layer_opacity.ts 18755/** 18756* Partial line-layer-opacity 18757* render the whole layer to a scratch FBO, then composite with `layerOpacity`. 18758* Applies opacity uniformly to the layer instead of accumulating alpha across overlapping segments. 18759*/ 18760function prepareDrawLayerOpacity(painter, layer, coords) { 18761 const context = painter.context; 18762 const compositeTarget = context.bindFramebuffer.get(); 18763 const compositeViewport = context.viewport.get(); 18764 const [, , width, height] = compositeViewport; 18765 bindLayerOpacity(painter, width, height); 18766 context.viewport.set([ 18767 0, 18768 0, 18769 width, 18770 height 18771 ]); 18772 context.clear({ 18773 color: Color.transparent, 18774 depth: 1, 18775 stencil: 0 18776 }); 18777 painter.currentStencilSource = void 0; 18778 painter.renderTileClippingMasks(layer, coords); 18779 return { 18780 compositeTarget, 18781 compositeViewport 18782 }; 18783} 18784function bindLayerOpacity(painter, width, height) { 18785 const gl = painter.context.gl; 18786 if (!painter.layerOpacityFbo) { 18787 const fbo = painter.context.createFramebuffer(width, height, true, true); 18788 const texture = gl.createTexture(); 18789 gl.bindTexture(gl.TEXTURE_2D, texture); 18790 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 18791 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 18792 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 18793 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 18794 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 18795 fbo.colorAttachment.set(texture); 18796 fbo.depthAttachment.set(painter.context.createRenderbuffer(gl.DEPTH_STENCIL, width, height)); 18797 painter.layerOpacityFbo = fbo; 18798 painter.context.bindFramebuffer.set(painter.layerOpacityFbo.framebuffer); 18799 return; 18800 } 18801 if (painter.layerOpacityFbo.width === width && painter.layerOpacityFbo.height === height) { 18802 painter.context.bindFramebuffer.set(painter.layerOpacityFbo.framebuffer); 18803 return; 18804 } 18805 const fbo = painter.layerOpacityFbo; 18806 gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); 18807 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 18808 painter.context.bindRenderbuffer.set(fbo.depthAttachment.get()); 18809 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height); 18810 painter.context.bindRenderbuffer.set(null); 18811 fbo.width = width; 18812 fbo.height = height; 18813 painter.context.bindFramebuffer.set(fbo.framebuffer); 18814} 18815function drawLayerOpacity(painter, opacity, prepareDrawLayerOpacityResult, layer) { 18816 const context = painter.context; 18817 const gl = context.gl; 18818 context.bindFramebuffer.set(prepareDrawLayerOpacityResult.compositeTarget); 18819 context.viewport.set(prepareDrawLayerOpacityResult.compositeViewport); 18820 context.activeTexture.set(gl.TEXTURE0); 18821 gl.bindTexture(gl.TEXTURE_2D, painter.layerOpacityFbo.colorAttachment.get()); 18822 painter.useProgram("layerOpacity").draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, layerOpacityUniformValues(opacity, 0), null, null, layer.id, painter.viewportBuffer, painter.quadTriangleIndexBuffer, painter.viewportSegments, layer.paint, painter.transform.zoom); 18823 painter.currentStencilSource = void 0; 18824} 18825//#endregion 18826//#region src/webgl/draw/draw_line.ts 18827function updateGradientTexture(painter, tileManager, context, gl, layer, bucket, coord, layerGradient) { 18828 let textureResolution = 256; 18829 if (layer.stepInterpolant) { 18830 const sourceMaxZoom = tileManager.getSource().maxzoom; 18831 const potentialOverzoom = coord.canonical.z === sourceMaxZoom ? Math.ceil(1 << painter.transform.maxZoom - coord.canonical.z) : 1; 18832 const maxTextureCoverage = bucket.maxLineLength / EXTENT * 1024 * potentialOverzoom; 18833 textureResolution = clamp(nextPowerOfTwo(maxTextureCoverage), 256, context.maxTextureSize); 18834 } 18835 layerGradient.gradient = renderColorRamp({ 18836 expression: layer.gradientExpression(), 18837 evaluationKey: "lineProgress", 18838 resolution: textureResolution, 18839 image: layerGradient.gradient || void 0, 18840 clips: bucket.lineClipsArray 18841 }); 18842 if (layerGradient.texture) layerGradient.texture.update(layerGradient.gradient); 18843 else layerGradient.texture = new Texture(context, layerGradient.gradient, gl.RGBA); 18844 layerGradient.version = layer.gradientVersion; 18845 return layerGradient.texture; 18846} 18847function bindImagePatternTextures(context, gl, tile, programConfiguration, crossfade) { 18848 context.activeTexture.set(gl.TEXTURE0); 18849 tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 18850 programConfiguration.updatePaintBuffers(crossfade); 18851} 18852function bindDasharrayTextures(painter, context, gl, programConfiguration, programChanged, crossfade) { 18853 if (programChanged || painter.lineAtlas.dirty) { 18854 context.activeTexture.set(gl.TEXTURE0); 18855 painter.lineAtlas.bind(context); 18856 } 18857 programConfiguration.updatePaintBuffers(crossfade); 18858} 18859function bindGradientTextures(painter, tileManager, context, gl, layer, bucket, coord) { 18860 const layerGradient = bucket.gradients[layer.id]; 18861 let gradientTexture = layerGradient.texture; 18862 if (layer.gradientVersion !== layerGradient.version) gradientTexture = updateGradientTexture(painter, tileManager, context, gl, layer, bucket, coord, layerGradient); 18863 context.activeTexture.set(gl.TEXTURE0); 18864 gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE); 18865} 18866function bindGradientAndDashTextures(painter, tileManager, context, gl, layer, bucket, coord, programConfiguration, crossfade) { 18867 const layerGradient = bucket.gradients[layer.id]; 18868 let gradientTexture = layerGradient.texture; 18869 if (layer.gradientVersion !== layerGradient.version) gradientTexture = updateGradientTexture(painter, tileManager, context, gl, layer, bucket, coord, layerGradient); 18870 context.activeTexture.set(gl.TEXTURE0); 18871 gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE); 18872 context.activeTexture.set(gl.TEXTURE1); 18873 painter.lineAtlas.bind(context); 18874 programConfiguration.updatePaintBuffers(crossfade); 18875} 18876function drawLine(painter, tileManager, layer, coords, renderContext) { 18877 if (renderContext.currentPass !== "translucent") return; 18878 const opacity = layer.paint.get("line-opacity"); 18879 const width = layer.paint.get("line-width"); 18880 const layerOpacity = layer.paint.get("line-layer-opacity"); 18881 if (opacity.constantOr(1) === 0 || width.constantOr(1) === 0 || layerOpacity === 0) return; 18882 if (layerOpacity < 1) { 18883 const results = prepareDrawLayerOpacity(painter, layer, coords); 18884 drawLineTiles(painter, tileManager, layer, coords, renderContext); 18885 drawLayerOpacity(painter, layerOpacity, results, layer); 18886 return; 18887 } 18888 drawLineTiles(painter, tileManager, layer, coords, renderContext); 18889} 18890function drawLineTiles(painter, tileManager, layer, coords, renderContext) { 18891 const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 18892 const colorMode = painter.colorModeForRenderPass(); 18893 const dasharrayProperty = layer.paint.get("line-dasharray"); 18894 const dasharray = dasharrayProperty.constantOr(1); 18895 const patternProperty = layer.paint.get("line-pattern"); 18896 const image = patternProperty.constantOr(1); 18897 const gradient = layer.paint.get("line-gradient"); 18898 const crossfade = layer.getCrossfadeParameters(); 18899 let programId; 18900 if (image) programId = "linePattern"; 18901 else if (dasharray && gradient) programId = "lineGradientSDF"; 18902 else if (dasharray) programId = "lineSDF"; 18903 else if (gradient) programId = "lineGradient"; 18904 else programId = "line"; 18905 const context = painter.context; 18906 const gl = context.gl; 18907 const transform = painter.transform; 18908 let firstTile = true; 18909 for (const coord of coords) { 18910 const tile = tileManager.getTile(coord); 18911 if (image && !tile.patternsLoaded()) continue; 18912 const bucket = tile.getBucket(layer); 18913 if (!bucket) continue; 18914 const programConfiguration = bucket.programConfigurations.get(layer.id); 18915 const prevProgram = painter.context.program.get(); 18916 const program = painter.useProgram(programId, programConfiguration); 18917 const programChanged = firstTile || program.program !== prevProgram; 18918 const terrainData = getTerrainDataForTile(renderContext, coord); 18919 const constantPattern = patternProperty.constantOr(null); 18920 const constantDasharray = dasharrayProperty?.constantOr(null); 18921 if (constantPattern && tile.imageAtlas) { 18922 const atlas = tile.imageAtlas; 18923 const posTo = atlas.patternPositions[constantPattern.to.toString()]; 18924 const posFrom = atlas.patternPositions[constantPattern.from.toString()]; 18925 if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); 18926 } else if (constantDasharray) { 18927 const round = layer.layout.get("line-cap").constantOr(null) === "round"; 18928 const dashTo = painter.lineAtlas.getDash(constantDasharray.to, round); 18929 const dashFrom = painter.lineAtlas.getDash(constantDasharray.from, round); 18930 programConfiguration.setConstantDashPositions(dashTo, dashFrom); 18931 } 18932 const projectionData = getProjectionDataForTile(renderContext, coord); 18933 const pixelRatio = transform.getPixelScale(); 18934 let uniformValues; 18935 if (image) { 18936 uniformValues = linePatternUniformValues(painter, tile, layer, pixelRatio, crossfade); 18937 bindImagePatternTextures(context, gl, tile, programConfiguration, crossfade); 18938 } else if (dasharray && gradient) { 18939 uniformValues = lineGradientSDFUniformValues(painter, tile, layer, pixelRatio, crossfade, bucket.lineClipsArray.length); 18940 bindGradientAndDashTextures(painter, tileManager, context, gl, layer, bucket, coord, programConfiguration, crossfade); 18941 } else if (dasharray) { 18942 uniformValues = lineSDFUniformValues(painter, tile, layer, pixelRatio, crossfade); 18943 bindDasharrayTextures(painter, context, gl, programConfiguration, programChanged, crossfade); 18944 } else if (gradient) { 18945 uniformValues = lineGradientUniformValues(painter, tile, layer, pixelRatio, bucket.lineClipsArray.length); 18946 bindGradientTextures(painter, tileManager, context, gl, layer, bucket, coord); 18947 } else uniformValues = lineUniformValues(painter, tile, layer, pixelRatio); 18948 const stencil = painter.stencilModeForClipping(coord); 18949 program.draw(context, gl.TRIANGLES, depthMode, stencil, colorMode, CullFaceMode.disabled, uniformValues, terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, painter.transform.zoom, programConfiguration, bucket.layoutVertexBuffer2); 18950 firstTile = false; 18951 } 18952} 18953//#endregion 18954//#region src/render/update_pattern_positions_in_program.ts 18955/** 18956* A simple helper shared by draw_fill and draw_fill_extrusions to find the correct pattern positions AND update program. 18957* For transitionable properties, especially 'fill-pattern' and 'fill-extrusion-pattern', while rendering certain frames 18958* tile.imageAtlas has been updated by worker to hold the new pattern only, but rendering code is still looking for the previous image. 18959* The mismatch was causing setConstantPatternPositions method not being called and pixelRatio was always the 18960* default of 1, instead of actual values set by original map.addImage. 18961* 18962* @param programConfiguration - to be used to set pattern position and device pixel ratio. 18963* @param propertyName - 'fill-pattern' or 'fill-extrusion-pattern' property key 18964* @param constantPattern - either 'fill-pattern' or 'fill-extrusion-pattern' property value 18965* @param tile - current tile being drawn 18966* @param layer - current layer being rendered 18967*/ 18968function updatePatternPositionsInProgram(programConfiguration, propertyName, constantPattern, tile, layer) { 18969 if (!constantPattern || !tile?.imageAtlas) return; 18970 const patternPositions = tile.imageAtlas.patternPositions; 18971 let posTo = patternPositions[constantPattern.to.toString()]; 18972 let posFrom = patternPositions[constantPattern.from.toString()]; 18973 if (!posTo && posFrom) posTo = posFrom; 18974 if (!posFrom && posTo) posFrom = posTo; 18975 if (!posTo || !posFrom) { 18976 const transitioned = layer.getPaintProperty(propertyName); 18977 posTo = patternPositions[transitioned]; 18978 posFrom = patternPositions[transitioned]; 18979 } 18980 if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); 18981} 18982//#endregion 18983//#region src/webgl/draw/draw_fill.ts 18984function drawFill(painter, tileManager, layer, coords, renderContext) { 18985 const color = layer.paint.get("fill-color"); 18986 const opacity = layer.paint.get("fill-opacity"); 18987 const layerOpacity = layer.paint.get("fill-layer-opacity"); 18988 if (opacity.constantOr(1) === 0 || layerOpacity === 0) return; 18989 if (layerOpacity < 1) { 18990 if (renderContext.currentPass !== "translucent") return; 18991 const results = prepareDrawLayerOpacity(painter, layer, coords); 18992 drawFillAndOutline(painter, tileManager, layer, coords, renderContext); 18993 drawLayerOpacity(painter, layerOpacity, results, layer); 18994 return; 18995 } 18996 const pattern = layer.paint.get("fill-pattern"); 18997 const fillEligibleForOpaque = painter.opaquePassEnabledForLayer() && !pattern.constantOr(1) && color.constantOr(Color.transparent).a === 1 && opacity.constantOr(0) === 1; 18998 if (fillEligibleForOpaque && renderContext.currentPass === "opaque") { 18999 const colorMode = painter.colorModeForRenderPass(); 19000 drawFillTiles(painter, tileManager, layer, coords, painter.getDepthModeForSublayer(1, DepthMode.ReadWrite), colorMode, false, renderContext); 19001 return; 19002 } 19003 if (fillEligibleForOpaque && renderContext.currentPass === "translucent") { 19004 drawOutline(painter, tileManager, layer, coords, renderContext); 19005 return; 19006 } 19007 if (renderContext.currentPass === "translucent") drawFillAndOutline(painter, tileManager, layer, coords, renderContext); 19008} 19009/** 19010* Draw fill + outline in a single translucent pass with ReadOnly depth. 19011* Shared by the layer-opacity subpass (always) and the normal translucent path 19012* (when the fill is not opaque-pass-eligible). 19013*/ 19014function drawFillAndOutline(painter, tileManager, layer, coords, renderContext) { 19015 const colorMode = painter.colorModeForRenderPass(); 19016 drawFillTiles(painter, tileManager, layer, coords, painter.getDepthModeForSublayer(1, DepthMode.ReadOnly), colorMode, false, renderContext); 19017 drawOutline(painter, tileManager, layer, coords, renderContext); 19018} 19019function drawOutline(painter, tileManager, layer, coords, renderContext) { 19020 if (!layer.paint.get("fill-antialias")) return; 19021 const colorMode = painter.colorModeForRenderPass(); 19022 drawFillTiles(painter, tileManager, layer, coords, painter.getDepthModeForSublayer(layer.getPaintProperty("fill-outline-color") ? 2 : 0, DepthMode.ReadOnly), colorMode, true, renderContext); 19023} 19024function drawFillTiles(painter, tileManager, layer, coords, depthMode, colorMode, isOutline, renderContext) { 19025 const gl = painter.context.gl; 19026 const fillPropertyName = "fill-pattern"; 19027 const patternProperty = layer.paint.get(fillPropertyName); 19028 const image = patternProperty?.constantOr(1); 19029 const crossfade = layer.getCrossfadeParameters(); 19030 let drawMode, programName, uniformValues, indexBuffer, segments; 19031 const transform = painter.transform; 19032 const propertyFillTranslate = layer.paint.get("fill-translate"); 19033 const propertyFillTranslateAnchor = layer.paint.get("fill-translate-anchor"); 19034 if (!isOutline) { 19035 programName = image ? "fillPattern" : "fill"; 19036 drawMode = gl.TRIANGLES; 19037 } else { 19038 programName = image && !layer.getPaintProperty("fill-outline-color") ? "fillOutlinePattern" : "fillOutline"; 19039 drawMode = gl.LINES; 19040 } 19041 const constantPattern = patternProperty.constantOr(null); 19042 for (const coord of coords) { 19043 const tile = tileManager.getTile(coord); 19044 if (image && !tile.patternsLoaded()) continue; 19045 const bucket = tile.getBucket(layer); 19046 if (!bucket) continue; 19047 const isSdfPattern = bucket.sdfPatterns[layer.id] ?? false; 19048 const programConfiguration = bucket.programConfigurations.get(layer.id); 19049 const program = painter.useProgram(programName, programConfiguration); 19050 const terrainData = getTerrainDataForTile(renderContext, coord); 19051 if (image) { 19052 painter.context.activeTexture.set(gl.TEXTURE0); 19053 tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 19054 programConfiguration.updatePaintBuffers(crossfade); 19055 } 19056 updatePatternPositionsInProgram(programConfiguration, fillPropertyName, constantPattern, tile, layer); 19057 const projectionData = getProjectionDataForTile(renderContext, coord); 19058 const translateForUniforms = translatePosition(transform, tile, propertyFillTranslate, propertyFillTranslateAnchor); 19059 if (!isOutline) { 19060 indexBuffer = bucket.indexBuffer; 19061 segments = bucket.segments; 19062 uniformValues = image ? fillPatternUniformValues(painter, crossfade, tile, translateForUniforms, isSdfPattern) : fillUniformValues(translateForUniforms); 19063 } else { 19064 indexBuffer = bucket.indexBuffer2; 19065 segments = bucket.segments2; 19066 uniformValues = programName === "fillOutlinePattern" && image ? fillOutlinePatternUniformValues(painter, crossfade, tile, translateForUniforms, isSdfPattern) : fillOutlineUniformValues(translateForUniforms); 19067 } 19068 const stencil = painter.stencilModeForClipping(coord); 19069 program.draw(painter.context, drawMode, depthMode, stencil, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, indexBuffer, segments, layer.paint, painter.transform.zoom, programConfiguration); 19070 } 19071} 19072//#endregion 19073//#region src/webgl/draw/draw_fill_extrusion.ts 19074function drawFillExtrusion(painter, tileManager, layer, coords, renderContext) { 19075 const opacity = layer.paint.get("fill-extrusion-opacity"); 19076 if (opacity === 0) return; 19077 if (renderContext.currentPass === "translucent") { 19078 const depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, renderContext.depthRangeFor3D); 19079 if (opacity === 1 && !layer.paint.get("fill-extrusion-pattern").constantOr(1)) { 19080 const colorMode = painter.colorModeForRenderPass(); 19081 drawExtrusionTiles(painter, tileManager, layer, coords, depthMode, StencilMode.disabled, colorMode, renderContext); 19082 } else { 19083 drawExtrusionTiles(painter, tileManager, layer, coords, depthMode, StencilMode.disabled, ColorMode.disabled, renderContext); 19084 drawExtrusionTiles(painter, tileManager, layer, coords, depthMode, painter.stencilModeFor3D(), painter.colorModeForRenderPass(), renderContext); 19085 } 19086 } 19087} 19088function drawExtrusionTiles(painter, tileManager, layer, coords, depthMode, stencilMode, colorMode, renderContext) { 19089 const context = painter.context; 19090 const gl = context.gl; 19091 const fillPropertyName = "fill-extrusion-pattern"; 19092 const patternProperty = layer.paint.get(fillPropertyName); 19093 const image = patternProperty.constantOr(1); 19094 const crossfade = layer.getCrossfadeParameters(); 19095 const opacity = layer.paint.get("fill-extrusion-opacity"); 19096 const constantPattern = patternProperty.constantOr(null); 19097 const transform = painter.transform; 19098 for (const coord of coords) { 19099 const tile = tileManager.getTile(coord); 19100 const bucket = tile.getBucket(layer); 19101 if (!bucket) continue; 19102 const terrainData = getTerrainDataForTile(renderContext, coord); 19103 const programConfiguration = bucket.programConfigurations.get(layer.id); 19104 const program = painter.useProgram(image ? "fillExtrusionPattern" : "fillExtrusion", programConfiguration); 19105 if (image) { 19106 painter.context.activeTexture.set(gl.TEXTURE0); 19107 tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 19108 programConfiguration.updatePaintBuffers(crossfade); 19109 } 19110 const projectionData = getProjectionDataForTile(renderContext, coord); 19111 updatePatternPositionsInProgram(programConfiguration, fillPropertyName, constantPattern, tile, layer); 19112 const translate = translatePosition(transform, tile, layer.paint.get("fill-extrusion-translate"), layer.paint.get("fill-extrusion-translate-anchor")); 19113 const shouldUseVerticalGradient = layer.paint.get("fill-extrusion-vertical-gradient"); 19114 const uniformValues = image ? fillExtrusionPatternUniformValues(painter, shouldUseVerticalGradient, opacity, translate, coord, crossfade, tile) : fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate); 19115 program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, painter.transform.zoom, programConfiguration, painter.style.map.terrain && bucket.centroidVertexBuffer); 19116 } 19117} 19118//#endregion 19119//#region src/webgl/draw/draw_hillshade.ts 19120function drawHillshade(painter, tileManager, layer, tileIDs, renderContext) { 19121 if (renderContext.currentPass !== "offscreen" && renderContext.currentPass !== "translucent") return; 19122 const context = painter.context; 19123 const useSubdivision = painter.style.projection.useSubdivision; 19124 const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 19125 const colorMode = painter.colorModeForRenderPass(); 19126 if (renderContext.currentPass === "offscreen") { 19127 prepareHillshade(painter, tileManager, tileIDs, layer, depthMode, StencilMode.disabled, colorMode); 19128 context.viewport.set([ 19129 0, 19130 0, 19131 painter.width, 19132 painter.height 19133 ]); 19134 } else if (renderContext.currentPass === "translucent") { 19135 if (useSubdivision) { 19136 const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs); 19137 renderHillshade(painter, tileManager, layer, coords, stencilBorderless, depthMode, colorMode, false, renderContext); 19138 renderHillshade(painter, tileManager, layer, coords, stencilBorders, depthMode, colorMode, true, renderContext); 19139 } else { 19140 const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs); 19141 renderHillshade(painter, tileManager, layer, coords, stencil, depthMode, colorMode, false, renderContext); 19142 } 19143 } 19144} 19145function renderHillshade(painter, tileManager, layer, coords, stencilModes, depthMode, colorMode, useBorder, renderContext) { 19146 const projection = painter.style.projection; 19147 const context = painter.context; 19148 const gl = context.gl; 19149 const defines = [`#define NUM_ILLUMINATION_SOURCES ${layer.paint.get("hillshade-highlight-color").values.length}`]; 19150 const program = painter.useProgram("hillshade", null, false, defines); 19151 const align = !painter.options.moving; 19152 for (const coord of coords) { 19153 const tile = tileManager.getTile(coord); 19154 const fbo = tile.fbo; 19155 if (!fbo) continue; 19156 const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, "raster"); 19157 const terrainData = getTerrainDataForTile(renderContext, coord); 19158 context.activeTexture.set(gl.TEXTURE0); 19159 gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); 19160 const projectionData = getProjectionDataForTile(renderContext, coord, { aligned: align }); 19161 program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.backCCW, hillshadeUniformValues(painter, tile, layer), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19162 } 19163} 19164function prepareHillshade(painter, tileManager, tileIDs, layer, depthMode, stencilMode, colorMode) { 19165 const context = painter.context; 19166 const gl = context.gl; 19167 const textureFilter = layer.paint.get("resampling") === "nearest" ? gl.NEAREST : gl.LINEAR; 19168 for (const coord of tileIDs) { 19169 const tile = tileManager.getTile(coord); 19170 const dem = tile.dem; 19171 if (!dem?.data) continue; 19172 if (!tile.needsHillshadePrepare) continue; 19173 const hillshadeTextureSize = dem.dim + 2; 19174 const textureStride = dem.stride; 19175 const pixelData = dem.getPixels(); 19176 context.activeTexture.set(gl.TEXTURE1); 19177 context.pixelStoreUnpackPremultiplyAlpha.set(false); 19178 tile.demTexture ||= painter.getTileTexture(textureStride); 19179 if (tile.demTexture) { 19180 const demTexture = tile.demTexture; 19181 demTexture.update(pixelData, { premultiply: false }); 19182 demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); 19183 } else { 19184 tile.demTexture = new Texture(context, pixelData, gl.RGBA, { premultiply: false }); 19185 tile.demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); 19186 } 19187 context.activeTexture.set(gl.TEXTURE0); 19188 let fbo = tile.fbo; 19189 if (!fbo) { 19190 const renderTexture = new Texture(context, { 19191 width: hillshadeTextureSize, 19192 height: hillshadeTextureSize, 19193 data: null 19194 }, gl.RGBA); 19195 renderTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); 19196 fbo = tile.fbo = context.createFramebuffer(hillshadeTextureSize, hillshadeTextureSize, true, false); 19197 fbo.colorAttachment.set(renderTexture.texture); 19198 } 19199 context.bindFramebuffer.set(fbo.framebuffer); 19200 context.viewport.set([ 19201 0, 19202 0, 19203 hillshadeTextureSize, 19204 hillshadeTextureSize 19205 ]); 19206 painter.useProgram("hillshadePrepare").draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, hillshadeUniformPrepareValues(tile.tileID, dem), null, null, layer.id, painter.rasterBoundsBuffer, painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); 19207 tile.needsHillshadePrepare = false; 19208 } 19209} 19210//#endregion 19211//#region src/webgl/draw/draw_color_relief.ts 19212function drawColorRelief(painter, tileManager, layer, tileIDs, renderContext) { 19213 if (renderContext.currentPass !== "translucent") return; 19214 if (!tileIDs.length) return; 19215 const useSubdivision = painter.style.projection.useSubdivision; 19216 const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 19217 const colorMode = painter.colorModeForRenderPass(); 19218 if (useSubdivision) { 19219 const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs); 19220 renderColorRelief(painter, tileManager, layer, coords, stencilBorderless, depthMode, colorMode, false, renderContext); 19221 renderColorRelief(painter, tileManager, layer, coords, stencilBorders, depthMode, colorMode, true, renderContext); 19222 } else { 19223 const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs); 19224 renderColorRelief(painter, tileManager, layer, coords, stencil, depthMode, colorMode, false, renderContext); 19225 } 19226} 19227let textureMaxSize = 0; 19228/** 19229* Draws the color-relief tiles of one pass. 19230* 19231* A loaded raster-DEM tile can carry no DEM at all (e.g. an empty 204 response); 19232* such tiles are skipped before the first-tile setup reads from them. 19233*/ 19234function renderColorRelief(painter, tileManager, layer, coords, stencilModes, depthMode, colorMode, useBorder, renderContext) { 19235 const projection = painter.style.projection; 19236 const context = painter.context; 19237 const gl = context.gl; 19238 const program = painter.useProgram("colorRelief"); 19239 const align = !painter.options.moving; 19240 const textureFilter = layer.paint.get("resampling") === "nearest" ? gl.NEAREST : gl.LINEAR; 19241 let firstTile = true; 19242 let colorRampSize = 0; 19243 for (const coord of coords) { 19244 const tile = tileManager.getTile(coord); 19245 const dem = tile.dem; 19246 if (!dem?.data) continue; 19247 if (firstTile) { 19248 textureMaxSize ||= gl.getParameter(gl.MAX_TEXTURE_SIZE); 19249 const maxLength = textureMaxSize; 19250 const { elevationTexture, colorTexture } = layer.getColorRampTextures(context, maxLength, dem.getUnpackVector()); 19251 context.activeTexture.set(gl.TEXTURE1); 19252 elevationTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); 19253 context.activeTexture.set(gl.TEXTURE4); 19254 colorTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 19255 firstTile = false; 19256 colorRampSize = elevationTexture.size[0]; 19257 } 19258 const textureStride = dem.stride; 19259 context.activeTexture.set(gl.TEXTURE0); 19260 if (!tile.demTexture || tile.needsColorReliefPrepare) { 19261 context.pixelStoreUnpackPremultiplyAlpha.set(false); 19262 tile.demTexture ||= painter.getTileTexture(textureStride) ?? new Texture(context, { 19263 width: textureStride, 19264 height: textureStride, 19265 data: null 19266 }, gl.RGBA); 19267 tile.demTexture.update(dem.getPixels(), { premultiply: false }); 19268 tile.needsColorReliefPrepare = false; 19269 } 19270 tile.demTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); 19271 const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, "raster"); 19272 const terrainData = getTerrainDataForTile(renderContext, coord); 19273 const projectionData = getProjectionDataForTile(renderContext, coord, { aligned: align }); 19274 program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.backCCW, colorReliefUniformValues(layer, tile.dem, colorRampSize), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19275 } 19276} 19277//#endregion 19278//#region src/webgl/draw/draw_raster.ts 19279const cornerCoords = [ 19280 new Point(0, 0), 19281 new Point(EXTENT, 0), 19282 new Point(EXTENT, EXTENT), 19283 new Point(0, EXTENT) 19284]; 19285function drawRaster(painter, tileManager, layer, tileIDs, renderContext) { 19286 if (renderContext.currentPass !== "translucent") return; 19287 if (layer.paint.get("raster-opacity") === 0) return; 19288 if (!tileIDs.length) return; 19289 const source = tileManager.getSource(); 19290 const useSubdivision = painter.style.projection.useSubdivision; 19291 if (source instanceof ImageSource) drawTiles(painter, tileManager, layer, tileIDs, null, false, false, source.tileCoords, source.imageWarp, source.flippedWindingOrder, renderContext, source.getMesh(painter.context, useSubdivision)); 19292 else if (useSubdivision) { 19293 const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs); 19294 drawTiles(painter, tileManager, layer, coords, stencilBorderless, false, true, cornerCoords, bilinearImageWarp, false, renderContext); 19295 drawTiles(painter, tileManager, layer, coords, stencilBorders, true, true, cornerCoords, bilinearImageWarp, false, renderContext); 19296 } else { 19297 const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs); 19298 drawTiles(painter, tileManager, layer, coords, stencil, false, true, cornerCoords, bilinearImageWarp, false, renderContext); 19299 } 19300} 19301function drawTiles(painter, tileManager, layer, coords, stencilModes, useBorder, allowPoles, corners, imageWarp, flipCullfaceMode = false, renderContext, sourceMesh = null) { 19302 const minTileZ = coords[coords.length - 1].overscaledZ; 19303 const context = painter.context; 19304 const gl = context.gl; 19305 const program = painter.useProgram("raster"); 19306 const projection = painter.style.projection; 19307 const colorMode = painter.colorModeForRenderPass(); 19308 const align = !painter.options.moving; 19309 const rasterOpacity = layer.paint.get("raster-opacity"); 19310 const textureFilter = layer.paint.get("resampling") === "nearest" || layer.paint.get("raster-resampling") === "nearest" ? gl.NEAREST : gl.LINEAR; 19311 const fadeDuration = layer.paint.get("raster-fade-duration"); 19312 const isTerrain = !!painter.style.map.terrain; 19313 for (const coord of coords) { 19314 const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ, rasterOpacity === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS); 19315 const tile = tileManager.getTile(coord); 19316 context.activeTexture.set(gl.TEXTURE0); 19317 tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); 19318 context.activeTexture.set(gl.TEXTURE1); 19319 const { parentTile, parentScaleBy, parentTopLeft, fadeValues } = getFadeProperties(tile, tileManager, fadeDuration, isTerrain); 19320 tile.fadeOpacity = fadeValues.tileOpacity; 19321 if (parentTile) { 19322 parentTile.fadeOpacity = fadeValues.parentTileOpacity; 19323 parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); 19324 } else tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); 19325 if (tile.texture.useMipmap && context.extTextureFilterAnisotropic && painter.transform.pitch > painter.options.anisotropicFilterPitch) gl.texParameterf(gl.TEXTURE_2D, context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, context.extTextureFilterAnisotropicMax); 19326 const terrainData = getTerrainDataForTile(renderContext, coord); 19327 const projectionData = getProjectionDataForTile(renderContext, coord, { aligned: align }); 19328 const uniformValues = rasterUniformValues(parentTopLeft, parentScaleBy, fadeValues.fadeMix, layer, corners, imageWarp); 19329 const mesh = sourceMesh ?? projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, "raster"); 19330 const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled; 19331 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, flipCullfaceMode ? CullFaceMode.frontCCW : CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19332 } 19333} 19334/** 19335* Get fade properties for current tile - either cross-fading or self-fading properties. 19336*/ 19337function getFadeProperties(tile, tileManager, fadeDuration, isTerrain) { 19338 const defaults = { 19339 parentTile: null, 19340 parentScaleBy: 1, 19341 parentTopLeft: [0, 0], 19342 fadeValues: { 19343 tileOpacity: 1, 19344 parentTileOpacity: 1, 19345 fadeMix: { 19346 opacity: 1, 19347 mix: 0 19348 } 19349 } 19350 }; 19351 if (fadeDuration === 0 || isTerrain) return defaults; 19352 if (tile.fadingParentID) { 19353 const parentTile = tileManager.getLoadedTile(tile.fadingParentID); 19354 if (!parentTile) return defaults; 19355 const parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ); 19356 return { 19357 parentTile, 19358 parentScaleBy, 19359 parentTopLeft: [tile.tileID.canonical.x * parentScaleBy % 1, tile.tileID.canonical.y * parentScaleBy % 1], 19360 fadeValues: getCrossFadeValues(tile, parentTile, fadeDuration) 19361 }; 19362 } 19363 if (tile.selfFading) return { 19364 parentTile: null, 19365 parentScaleBy: 1, 19366 parentTopLeft: [0, 0], 19367 fadeValues: getSelfFadeValues(tile, fadeDuration) 19368 }; 19369 return defaults; 19370} 19371/** 19372* Cross-fade values for a base tile with a parent tile (for zooming in/out) 19373*/ 19374function getCrossFadeValues(tile, parentTile, fadeDuration) { 19375 const currentTime = now(); 19376 const timeSinceTile = (currentTime - tile.timeAdded) / fadeDuration; 19377 const timeSinceParent = (currentTime - parentTile.timeAdded) / fadeDuration; 19378 const doFadeIn = tile.fadingDirection === 1; 19379 const opacity1 = clamp(timeSinceTile, 0, 1); 19380 const opacity2 = clamp(1 - timeSinceParent, 0, 1); 19381 const tileOpacity = doFadeIn ? opacity1 : opacity2; 19382 return { 19383 tileOpacity, 19384 parentTileOpacity: doFadeIn ? opacity2 : opacity1, 19385 fadeMix: { 19386 opacity: 1, 19387 mix: 1 - tileOpacity 19388 } 19389 }; 19390} 19391/** 19392* Simple fade-in values for tile without a parent (i.e. edge tiles) 19393*/ 19394function getSelfFadeValues(tile, fadeDuration) { 19395 const timeSinceTile = (now() - tile.timeAdded) / fadeDuration; 19396 const tileOpacity = clamp(timeSinceTile, 0, 1); 19397 return { 19398 tileOpacity, 19399 fadeMix: { 19400 opacity: tileOpacity, 19401 mix: 0 19402 } 19403 }; 19404} 19405//#endregion 19406//#region src/webgl/draw/draw_background.ts 19407function drawBackground(painter, tileManager, layer, coords, renderContext) { 19408 const color = layer.paint.get("background-color"); 19409 const opacity = layer.paint.get("background-opacity"); 19410 if (opacity === 0) return; 19411 const context = painter.context; 19412 const gl = context.gl; 19413 const projection = painter.style.projection; 19414 const transform = painter.transform; 19415 const tileSize = transform.tileSize; 19416 const image = layer.paint.get("background-pattern"); 19417 if (painter.isPatternMissing(image)) return; 19418 const pass = !image && color.a === 1 && opacity === 1 && painter.opaquePassEnabledForLayer() ? "opaque" : "translucent"; 19419 if (renderContext.currentPass !== pass) return; 19420 const stencilMode = StencilMode.disabled; 19421 const depthMode = painter.getDepthModeForSublayer(0, pass === "opaque" ? DepthMode.ReadWrite : DepthMode.ReadOnly); 19422 const colorMode = painter.colorModeForRenderPass(); 19423 const program = painter.useProgram(image ? "backgroundPattern" : "background"); 19424 const tileIDs = coords ? coords : coveringTiles(transform, { 19425 tileSize, 19426 terrain: painter.style.map.terrain 19427 }); 19428 if (image) { 19429 context.activeTexture.set(gl.TEXTURE0); 19430 painter.patternAtlas.bind(painter.context); 19431 } 19432 const crossfade = layer.getCrossfadeParameters(); 19433 for (const tileID of tileIDs) { 19434 const projectionData = getProjectionDataForTile(renderContext, tileID); 19435 const uniformValues = image ? backgroundPatternUniformValues(opacity, painter, image, { 19436 tileID, 19437 tileSize 19438 }, crossfade) : backgroundUniformValues(opacity, color); 19439 const terrainData = getTerrainDataForTile(renderContext, tileID); 19440 const mesh = projection.getMeshFromTileID(context, tileID.canonical, false, true, "raster"); 19441 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19442 } 19443} 19444//#endregion 19445//#region src/webgl/draw/draw_debug.ts 19446const topColor = new Color(1, 0, 0, 1); 19447const btmColor = new Color(0, 1, 0, 1); 19448const leftColor = new Color(0, 0, 1, 1); 19449const rightColor = new Color(1, 0, 1, 1); 19450const centerColor = new Color(0, 1, 1, 1); 19451function drawDebugPadding(painter) { 19452 const padding = painter.transform.padding; 19453 const lineWidth = 3; 19454 drawHorizontalLine(painter, painter.transform.height - (padding.top || 0), lineWidth, topColor); 19455 drawHorizontalLine(painter, padding.bottom || 0, lineWidth, btmColor); 19456 drawVerticalLine(painter, padding.left || 0, lineWidth, leftColor); 19457 drawVerticalLine(painter, painter.transform.width - (padding.right || 0), lineWidth, rightColor); 19458 const center = painter.transform.centerPoint; 19459 drawCrosshair(painter, center.x, painter.transform.height - center.y, centerColor); 19460} 19461function drawCrosshair(painter, x, y, color) { 19462 const size = 20; 19463 const lineWidth = 2; 19464 drawDebugSSRect(painter, x - lineWidth / 2, y - size / 2, lineWidth, size, color); 19465 drawDebugSSRect(painter, x - size / 2, y - lineWidth / 2, size, lineWidth, color); 19466} 19467function drawHorizontalLine(painter, y, lineWidth, color) { 19468 drawDebugSSRect(painter, 0, y + lineWidth / 2, painter.transform.width, lineWidth, color); 19469} 19470function drawVerticalLine(painter, x, lineWidth, color) { 19471 drawDebugSSRect(painter, x - lineWidth / 2, 0, lineWidth, painter.transform.height, color); 19472} 19473function drawDebugSSRect(painter, x, y, width, height, color) { 19474 const context = painter.context; 19475 const gl = context.gl; 19476 gl.enable(gl.SCISSOR_TEST); 19477 gl.scissor(x * painter.pixelRatio, y * painter.pixelRatio, width * painter.pixelRatio, height * painter.pixelRatio); 19478 context.clear({ color }); 19479 gl.disable(gl.SCISSOR_TEST); 19480} 19481function drawDebug(painter, tileManager, coords, renderContext) { 19482 for (const coord of coords) drawDebugTile(painter, tileManager, coord, renderContext); 19483} 19484function drawDebugTile(painter, tileManager, coord, renderContext) { 19485 const context = painter.context; 19486 const gl = context.gl; 19487 const program = painter.useProgram("debug"); 19488 const depthMode = DepthMode.disabled; 19489 const stencilMode = StencilMode.disabled; 19490 const colorMode = painter.colorModeForRenderPass(); 19491 const id = "$debug"; 19492 const terrainData = getTerrainDataForTile(renderContext, coord); 19493 context.activeTexture.set(gl.TEXTURE0); 19494 const tileByteLength = tileManager.getTileByID(coord.key).latestRawTileData?.byteLength || 0; 19495 const tileSizeKb = Math.floor(tileByteLength / 1024); 19496 const tileSize = tileManager.getTile(coord).tileSize; 19497 const scaleRatio = 512 / Math.min(tileSize, 512) * (coord.overscaledZ / painter.transform.zoom) * .5; 19498 let tileIdText = coord.canonical.toString(); 19499 if (coord.overscaledZ !== coord.canonical.z) tileIdText += ` => ${coord.overscaledZ}`; 19500 drawTextToOverlay(painter, `${tileIdText} ${tileSizeKb}kB`); 19501 const projectionData = getProjectionDataForTile(renderContext, coord); 19502 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, ColorMode.alphaBlended, CullFaceMode.disabled, debugUniformValues(Color.transparent, scaleRatio), null, projectionData, id, painter.debugBuffer, painter.quadTriangleIndexBuffer, painter.debugSegments); 19503 program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled, debugUniformValues(Color.red), terrainData, projectionData, id, painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments); 19504} 19505function drawTextToOverlay(painter, text) { 19506 painter.initDebugOverlayCanvas(); 19507 const canvas = painter.debugOverlayCanvas; 19508 const gl = painter.context.gl; 19509 const ctx2d = painter.debugOverlayCanvas.getContext("2d"); 19510 ctx2d.clearRect(0, 0, canvas.width, canvas.height); 19511 ctx2d.shadowColor = "white"; 19512 ctx2d.shadowBlur = 2; 19513 ctx2d.lineWidth = 1.5; 19514 ctx2d.strokeStyle = "white"; 19515 ctx2d.textBaseline = "top"; 19516 ctx2d.font = `bold 36px Open Sans, sans-serif`; 19517 ctx2d.fillText(text, 5, 5); 19518 ctx2d.strokeText(text, 5, 5); 19519 painter.debugOverlayTexture.update(canvas); 19520 painter.debugOverlayTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); 19521} 19522function selectDebugSource(style, zoom) { 19523 let selectedSource = null; 19524 const sources = Object.values(style._layers).flatMap((layer) => { 19525 if (layer.source && !layer.isHidden(zoom)) return [style.tileManagers[layer.source]]; 19526 else return []; 19527 }); 19528 const vectorSources = sources.filter((source) => source.getSource().type === "vector"); 19529 const otherSources = sources.filter((source) => source.getSource().type !== "vector"); 19530 const considerSource = (source) => { 19531 if (!selectedSource || selectedSource.getSource().maxzoom < source.getSource().maxzoom) selectedSource = source; 19532 }; 19533 for (const source of vectorSources) considerSource(source); 19534 if (!selectedSource) for (const source of otherSources) considerSource(source); 19535 return selectedSource; 19536} 19537//#endregion 19538//#region src/webgl/draw/draw_custom.ts 19539function drawCustom(painter, tileManager, layer, renderContext) { 19540 const { isRenderingGlobe } = renderContext; 19541 const context = painter.context; 19542 const implementation = layer.implementation; 19543 const projection = painter.style.projection; 19544 const transform = painter.transform; 19545 const projectionData = transform.getProjectionDataForCustomLayer(isRenderingGlobe); 19546 const customLayerArgs = { 19547 farZ: transform.farZ, 19548 nearZ: transform.nearZ, 19549 fov: transform.fov * Math.PI / 180, 19550 modelViewProjectionMatrix: transform.modelViewProjectionMatrix, 19551 projectionMatrix: transform.projectionMatrix, 19552 shaderData: { 19553 variantName: projection.shaderVariantName, 19554 vertexShaderPrelude: `const float PI = 3.141592653589793;\nuniform mat4 u_projection_matrix;\n${projection.shaderPreludeCode.vertexSource}`, 19555 define: projection.shaderDefine 19556 }, 19557 defaultProjectionData: projectionData, 19558 getProjectionData: (params) => { 19559 return transform.getProjectionData({ 19560 overscaledTileID: new OverscaledTileID(params.tileID.canonical.z, params.tileID.wrap ?? 0, params.tileID.canonical.z, params.tileID.canonical.x, params.tileID.canonical.y), 19561 aligned: params.aligned, 19562 applyGlobeMatrix: params.applyGlobeMatrix, 19563 applyTerrainMatrix: params.applyTerrainMatrix 19564 }); 19565 } 19566 }; 19567 const renderingMode = implementation.renderingMode ? implementation.renderingMode : "2d"; 19568 if (renderContext.currentPass === "offscreen") { 19569 const prerender = implementation.prerender; 19570 if (prerender) { 19571 painter.setCustomLayerDefaults(); 19572 context.setColorMode(painter.colorModeForRenderPass()); 19573 prerender.call(implementation, context.gl, customLayerArgs); 19574 context.setDirty(); 19575 painter.setBaseState(); 19576 } 19577 } else if (renderContext.currentPass === "translucent") { 19578 painter.setCustomLayerDefaults(); 19579 context.setColorMode(painter.colorModeForRenderPass()); 19580 context.setStencilMode(StencilMode.disabled); 19581 const depthMode = renderingMode === "3d" ? painter.getDepthModeFor3D() : painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); 19582 context.setDepthMode(depthMode); 19583 implementation.render(context.gl, customLayerArgs); 19584 context.setDirty(); 19585 painter.setBaseState(); 19586 context.bindFramebuffer.set(null); 19587 } 19588} 19589//#endregion 19590//#region src/webgl/draw/draw_terrain.ts 19591/** 19592* Redraw the Depth Framebuffer 19593* @param painter - the painter 19594* @param terrain - the terrain 19595*/ 19596function drawDepth(painter, terrain) { 19597 const context = painter.context; 19598 const gl = context.gl; 19599 const tr = painter.transform; 19600 const colorMode = ColorMode.unblended; 19601 const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, [0, 1]); 19602 const tiles = terrain.tileManager.getRenderableTiles(); 19603 const program = painter.useProgram("terrainDepth"); 19604 context.bindFramebuffer.set(terrain.getFramebuffer().framebuffer); 19605 context.viewport.set([ 19606 0, 19607 0, 19608 painter.width / devicePixelRatio, 19609 painter.height / devicePixelRatio 19610 ]); 19611 context.clear({ 19612 color: Color.white, 19613 depth: 1 19614 }); 19615 for (const tile of tiles) { 19616 const mesh = terrain.getTerrainMesh(tile.tileID); 19617 const terrainData = terrain.getTerrainData(tile.tileID); 19618 const projectionData = tr.getProjectionData({ 19619 overscaledTileID: tile.tileID, 19620 applyTerrainMatrix: false, 19621 applyGlobeMatrix: true 19622 }); 19623 const uniformValues = terrainDepthUniformValues(terrain.getSkirtLength(tr.zoom)); 19624 program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, "terrain", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19625 } 19626 context.bindFramebuffer.set(null); 19627 context.viewport.set([ 19628 0, 19629 0, 19630 painter.width, 19631 painter.height 19632 ]); 19633} 19634function drawTerrain(painter, terrain, tiles, renderContext) { 19635 const { isRenderingGlobe } = renderContext; 19636 const context = painter.context; 19637 const gl = context.gl; 19638 const tr = painter.transform; 19639 const colorMode = painter.colorModeForRenderPass(); 19640 const depthMode = painter.getDepthModeFor3D(); 19641 const program = painter.useProgram("terrain"); 19642 context.bindFramebuffer.set(null); 19643 context.viewport.set([ 19644 0, 19645 0, 19646 painter.width, 19647 painter.height 19648 ]); 19649 for (const tile of tiles) { 19650 const mesh = terrain.getTerrainMesh(tile.tileID); 19651 const texture = painter.renderToTexture.getTexture(tile); 19652 const terrainData = terrain.getTerrainData(tile.tileID); 19653 context.activeTexture.set(gl.TEXTURE0); 19654 texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_LINEAR); 19655 const eleDelta = terrain.getSkirtLength(tr.zoom); 19656 const fogMatrix = tr.calculateFogMatrix(tile.tileID.toUnwrapped()); 19657 const uniformValues = terrainUniformValues(eleDelta, fogMatrix, painter.style.sky, tr.pitch, isRenderingGlobe); 19658 const projectionData = getProjectionDataForTile(renderContext, tile.tileID, { applyTerrainMatrix: false }); 19659 program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, "terrain", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19660 } 19661} 19662//#endregion 19663//#region src/webgl/draw/draw_sky.ts 19664function getMesh(context, sky) { 19665 if (!sky.mesh) { 19666 const vertexArray = new PosArray(); 19667 vertexArray.emplaceBack(-1, -1); 19668 vertexArray.emplaceBack(1, -1); 19669 vertexArray.emplaceBack(1, 1); 19670 vertexArray.emplaceBack(-1, 1); 19671 const indexArray = new TriangleIndexArray(); 19672 indexArray.emplaceBack(0, 1, 2); 19673 indexArray.emplaceBack(0, 2, 3); 19674 sky.mesh = new Mesh(context.createVertexBuffer(vertexArray, posAttributes.members), context.createIndexBuffer(indexArray), SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length)); 19675 } 19676 return sky.mesh; 19677} 19678function drawSky(painter, sky) { 19679 const context = painter.context; 19680 const gl = context.gl; 19681 const skyUniforms = skyUniformValues(sky, painter.transform, painter.pixelRatio); 19682 const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, [0, 1]); 19683 const stencilMode = StencilMode.disabled; 19684 const colorMode = painter.colorModeForRenderPass(); 19685 const program = painter.useProgram("sky"); 19686 const mesh = getMesh(context, sky); 19687 program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, skyUniforms, null, void 0, "sky", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19688} 19689function getSunPos(light, transform) { 19690 const lightPos = light.getCartesianPosition(); 19691 negate(lightPos, lightPos); 19692 const lightMat = identity(/* @__PURE__ */ new Float64Array(16)); 19693 if (light.properties.get("anchor") === "map") { 19694 rotateZ(lightMat, lightMat, transform.rollInRadians); 19695 rotateX(lightMat, lightMat, -transform.pitchInRadians); 19696 rotateZ(lightMat, lightMat, transform.bearingInRadians); 19697 rotateX(lightMat, lightMat, transform.center.lat * Math.PI / 180); 19698 rotateY$1(lightMat, lightMat, -transform.center.lng * Math.PI / 180); 19699 } 19700 transformMat4$1(lightPos, lightPos, lightMat); 19701 return lightPos; 19702} 19703function drawAtmosphere(painter, sky, light) { 19704 const context = painter.context; 19705 const gl = context.gl; 19706 const program = painter.useProgram("atmosphere"); 19707 const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadOnly, [0, 1]); 19708 const transform = painter.transform; 19709 const sunPos = getSunPos(light, painter.transform); 19710 const projectionData = transform.getProjectionData({ 19711 overscaledTileID: null, 19712 applyGlobeMatrix: true, 19713 applyTerrainMatrix: true 19714 }); 19715 const globeRadius = getGlobeRadiusPixels(transform.worldSize, transform.center.lat); 19716 const globePosition = getGlobeCenterInViewSpace(transform); 19717 const altitudeBlend = getAtmosphereAltitudeBlend(length(globePosition) - globeRadius, globeRadius); 19718 const atmosphereBlend = sky.properties.get("atmosphere-blend") * projectionData.projectionTransition * altitudeBlend; 19719 if (atmosphereBlend === 0) return; 19720 const uniformValues = atmosphereUniformValues(sunPos, atmosphereBlend, globePosition, globeRadius, transform.inverseProjectionMatrix); 19721 const mesh = getMesh(context, sky); 19722 program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.alphaBlended, CullFaceMode.disabled, uniformValues, null, null, "atmosphere", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19723} 19724//#endregion 19725//#region src/webgl/draw/index.ts 19726const webglDrawFunctions = { 19727 symbol: drawSymbols, 19728 circle: drawCircles, 19729 heatmap: drawHeatmap, 19730 line: drawLine, 19731 fill: drawFill, 19732 fillExtrusion: drawFillExtrusion, 19733 hillshade: drawHillshade, 19734 colorRelief: drawColorRelief, 19735 raster: drawRaster, 19736 background: drawBackground, 19737 sky: drawSky, 19738 atmosphere: drawAtmosphere, 19739 custom: drawCustom, 19740 debug: drawDebug, 19741 debugPadding: drawDebugPadding, 19742 terrainDepth: drawDepth 19743}; 19744//#endregion 19745//#region src/render/painter.ts 19746/** 19747* @internal 19748* Initialize a new painter object. 19749*/ 19750var Painter = class Painter { 19751 constructor(gl, transform) { 19752 this.drawFunctions = webglDrawFunctions; 19753 this.context = new Context(gl); 19754 this.transform = transform; 19755 this.layerOpacityFbo = null; 19756 this._tileTextures = {}; 19757 this._rttObjectRecyclePool = []; 19758 this._rttSharedFbo = null; 19759 this.terrainFacilitator = { 19760 depthDirty: true, 19761 matrix: identity(/* @__PURE__ */ new Float64Array(16)), 19762 renderTime: 0 19763 }; 19764 this.setup(); 19765 this.numSublayers = TileManager.maxOverzooming + TileManager.maxUnderzooming + 1; 19766 this.depthEpsilon = 1 / Math.pow(2, 16); 19767 this.crossTileSymbolIndex = new CrossTileSymbolIndex(); 19768 } 19769 resize(width, height, pixelRatio) { 19770 this.width = Math.floor(width * pixelRatio); 19771 this.height = Math.floor(height * pixelRatio); 19772 this.pixelRatio = pixelRatio; 19773 this.context.viewport.set([ 19774 0, 19775 0, 19776 this.width, 19777 this.height 19778 ]); 19779 if (this.style) for (const layerId of this.style._order) this.style._layers[layerId].resize(); 19780 } 19781 setup() { 19782 const context = this.context; 19783 const tileExtentArray = new PosArray(); 19784 tileExtentArray.emplaceBack(0, 0); 19785 tileExtentArray.emplaceBack(EXTENT, 0); 19786 tileExtentArray.emplaceBack(0, EXTENT); 19787 tileExtentArray.emplaceBack(EXTENT, EXTENT); 19788 this.tileExtentBuffer = context.createVertexBuffer(tileExtentArray, posAttributes.members); 19789 this.tileExtentSegments = SegmentVector.simpleSegment(0, 0, 4, 2); 19790 const debugArray = new PosArray(); 19791 debugArray.emplaceBack(0, 0); 19792 debugArray.emplaceBack(EXTENT, 0); 19793 debugArray.emplaceBack(0, EXTENT); 19794 debugArray.emplaceBack(EXTENT, EXTENT); 19795 this.debugBuffer = context.createVertexBuffer(debugArray, posAttributes.members); 19796 this.debugSegments = SegmentVector.simpleSegment(0, 0, 4, 5); 19797 const rasterBoundsArray = new RasterBoundsArray(); 19798 rasterBoundsArray.emplaceBack(0, 0, 0, 0); 19799 rasterBoundsArray.emplaceBack(EXTENT, 0, EXTENT, 0); 19800 rasterBoundsArray.emplaceBack(0, EXTENT, 0, EXTENT); 19801 rasterBoundsArray.emplaceBack(EXTENT, EXTENT, EXTENT, EXTENT); 19802 this.rasterBoundsBuffer = context.createVertexBuffer(rasterBoundsArray, rasterBoundsAttributes.members); 19803 this.rasterBoundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); 19804 const rasterBoundsArrayPosOnly = new PosArray(); 19805 rasterBoundsArrayPosOnly.emplaceBack(0, 0); 19806 rasterBoundsArrayPosOnly.emplaceBack(EXTENT, 0); 19807 rasterBoundsArrayPosOnly.emplaceBack(0, EXTENT); 19808 rasterBoundsArrayPosOnly.emplaceBack(EXTENT, EXTENT); 19809 this.rasterBoundsBufferPosOnly = context.createVertexBuffer(rasterBoundsArrayPosOnly, posAttributes.members); 19810 this.rasterBoundsSegmentsPosOnly = SegmentVector.simpleSegment(0, 0, 4, 5); 19811 const viewportArray = new PosArray(); 19812 viewportArray.emplaceBack(0, 0); 19813 viewportArray.emplaceBack(1, 0); 19814 viewportArray.emplaceBack(0, 1); 19815 viewportArray.emplaceBack(1, 1); 19816 this.viewportBuffer = context.createVertexBuffer(viewportArray, posAttributes.members); 19817 this.viewportSegments = SegmentVector.simpleSegment(0, 0, 4, 2); 19818 const tileLineStripIndices = new LineStripIndexArray(); 19819 tileLineStripIndices.emplaceBack(0); 19820 tileLineStripIndices.emplaceBack(1); 19821 tileLineStripIndices.emplaceBack(3); 19822 tileLineStripIndices.emplaceBack(2); 19823 tileLineStripIndices.emplaceBack(0); 19824 this.tileBorderIndexBuffer = context.createIndexBuffer(tileLineStripIndices); 19825 const quadTriangleIndices = new TriangleIndexArray(); 19826 quadTriangleIndices.emplaceBack(1, 0, 2); 19827 quadTriangleIndices.emplaceBack(1, 2, 3); 19828 this.quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices); 19829 const gl = this.context.gl; 19830 this.stencilClearMode = new StencilMode({ 19831 func: gl.ALWAYS, 19832 mask: 0 19833 }, 0, 255, gl.ZERO, gl.ZERO, gl.ZERO); 19834 this.tileExtentMesh = new Mesh(this.tileExtentBuffer, this.quadTriangleIndexBuffer, this.tileExtentSegments); 19835 } 19836 clearStencil() { 19837 const context = this.context; 19838 const gl = context.gl; 19839 this.nextStencilID = 1; 19840 this.currentStencilSource = void 0; 19841 const matrix = create$1(); 19842 ortho(matrix, 0, this.width, this.height, 0, 0, 1); 19843 scale(matrix, matrix, [ 19844 gl.drawingBufferWidth, 19845 gl.drawingBufferHeight, 19846 0 19847 ]); 19848 const projectionData = { 19849 mainMatrix: matrix, 19850 tileMercatorCoords: [ 19851 0, 19852 0, 19853 1, 19854 1 19855 ], 19856 clippingPlane: [ 19857 0, 19858 0, 19859 0, 19860 0 19861 ], 19862 projectionTransition: 0, 19863 fallbackMatrix: matrix, 19864 clipAntimeridian: false 19865 }; 19866 this.useProgram("clippingMask", null, true).draw(context, gl.TRIANGLES, DepthMode.disabled, this.stencilClearMode, ColorMode.disabled, CullFaceMode.disabled, null, null, projectionData, "$clipping", this.viewportBuffer, this.quadTriangleIndexBuffer, this.viewportSegments); 19867 } 19868 renderTileClippingMasks(layer, tileIDs) { 19869 if (this.currentStencilSource === layer.source || !layer.isTileClipped() || !tileIDs?.length) return; 19870 this.currentStencilSource = layer.source; 19871 if (this.nextStencilID + tileIDs.length > 256) this.clearStencil(); 19872 const context = this.context; 19873 context.setColorMode(ColorMode.disabled); 19874 context.setDepthMode(DepthMode.disabled); 19875 const stencilRefs = {}; 19876 for (const tileID of tileIDs) stencilRefs[tileID.key] = this.nextStencilID++; 19877 if (this.style.projection.useSubdivision) this._renderTileMasks(stencilRefs, tileIDs, true); 19878 this._renderTileMasks(stencilRefs, tileIDs, false); 19879 this._tileClippingMaskIDs = stencilRefs; 19880 } 19881 _renderTileMasks(tileStencilRefs, tileIDs, useBorders) { 19882 const context = this.context; 19883 const gl = context.gl; 19884 const projection = this.style.projection; 19885 const renderContext = this.renderContext; 19886 const program = this.useProgram("clippingMask"); 19887 for (const tileID of tileIDs) { 19888 const stencilRef = tileStencilRefs[tileID.key]; 19889 const terrainData = getTerrainDataForTile(renderContext, tileID); 19890 const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true, "stencil"); 19891 const projectionData = getProjectionDataForTile(renderContext, tileID); 19892 program.draw(context, gl.TRIANGLES, DepthMode.disabled, new StencilMode({ 19893 func: gl.ALWAYS, 19894 mask: 0 19895 }, stencilRef, 255, gl.KEEP, gl.KEEP, gl.REPLACE), ColorMode.disabled, renderContext.isRenderingToTexture ? CullFaceMode.disabled : CullFaceMode.backCCW, null, terrainData, projectionData, "$clipping", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19896 } 19897 } 19898 /** 19899 * Fills the depth buffer with the geometry of all supplied tiles. 19900 * Does not change the color buffer or the stencil buffer. 19901 */ 19902 _renderTilesDepthBuffer() { 19903 const context = this.context; 19904 const gl = context.gl; 19905 const projection = this.style.projection; 19906 const transform = this.transform; 19907 const program = this.useProgram("depth"); 19908 const depthMode = this.getDepthModeFor3D(); 19909 const tileIDs = coveringTiles(transform, { tileSize: transform.tileSize }); 19910 for (const tileID of tileIDs) { 19911 const terrainData = getTerrainDataForTile(this.renderContext, tileID); 19912 const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, true, true, "raster"); 19913 const projectionData = getProjectionDataForTile(this.renderContext, tileID); 19914 program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.disabled, CullFaceMode.backCCW, null, terrainData, projectionData, "$clipping", mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); 19915 } 19916 } 19917 stencilModeFor3D() { 19918 this.currentStencilSource = void 0; 19919 if (this.nextStencilID + 1 > 256) this.clearStencil(); 19920 const id = this.nextStencilID++; 19921 const gl = this.context.gl; 19922 return new StencilMode({ 19923 func: gl.NOTEQUAL, 19924 mask: 255 19925 }, id, 255, gl.KEEP, gl.KEEP, gl.REPLACE); 19926 } 19927 stencilModeForClipping(tileID) { 19928 const gl = this.context.gl; 19929 return new StencilMode({ 19930 func: gl.EQUAL, 19931 mask: 255 19932 }, this._tileClippingMaskIDs[tileID.key], 0, gl.KEEP, gl.KEEP, gl.REPLACE); 19933 } 19934 getStencilConfigForOverlapAndUpdateStencilID(tileIDs) { 19935 const gl = this.context.gl; 19936 const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ); 19937 const minTileZ = coords[coords.length - 1].overscaledZ; 19938 const stencilValues = coords[0].overscaledZ - minTileZ + 1; 19939 if (stencilValues > 1) { 19940 this.currentStencilSource = void 0; 19941 if (this.nextStencilID + stencilValues > 256) this.clearStencil(); 19942 const zToStencilMode = {}; 19943 for (let i = 0; i < stencilValues; i++) zToStencilMode[i + minTileZ] = new StencilMode({ 19944 func: gl.GEQUAL, 19945 mask: 255 19946 }, i + this.nextStencilID, 255, gl.KEEP, gl.KEEP, gl.REPLACE); 19947 this.nextStencilID += stencilValues; 19948 return [zToStencilMode, coords]; 19949 } 19950 return [{ [minTileZ]: StencilMode.disabled }, coords]; 19951 } 19952 stencilConfigForOverlapTwoPass(tileIDs) { 19953 const gl = this.context.gl; 19954 const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ); 19955 const minTileZ = coords[coords.length - 1].overscaledZ; 19956 const stencilValues = coords[0].overscaledZ - minTileZ + 1; 19957 this.clearStencil(); 19958 if (stencilValues > 1) { 19959 const zToStencilModeHigh = {}; 19960 const zToStencilModeLow = {}; 19961 for (let i = 0; i < stencilValues; i++) { 19962 zToStencilModeHigh[i + minTileZ] = new StencilMode({ 19963 func: gl.GREATER, 19964 mask: 255 19965 }, stencilValues + 1 + i, 255, gl.KEEP, gl.KEEP, gl.REPLACE); 19966 zToStencilModeLow[i + minTileZ] = new StencilMode({ 19967 func: gl.GREATER, 19968 mask: 255 19969 }, 1 + i, 255, gl.KEEP, gl.KEEP, gl.REPLACE); 19970 } 19971 this.nextStencilID = stencilValues * 2 + 1; 19972 return [ 19973 zToStencilModeHigh, 19974 zToStencilModeLow, 19975 coords 19976 ]; 19977 } else { 19978 this.nextStencilID = 3; 19979 return [ 19980 { [minTileZ]: new StencilMode({ 19981 func: gl.GREATER, 19982 mask: 255 19983 }, 2, 255, gl.KEEP, gl.KEEP, gl.REPLACE) }, 19984 { [minTileZ]: new StencilMode({ 19985 func: gl.GREATER, 19986 mask: 255 19987 }, 1, 255, gl.KEEP, gl.KEEP, gl.REPLACE) }, 19988 coords 19989 ]; 19990 } 19991 } 19992 colorModeForRenderPass() { 19993 const gl = this.context.gl; 19994 if (this._showOverdrawInspector) { 19995 const a = 1 / 8; 19996 return new ColorMode([gl.CONSTANT_COLOR, gl.ONE], new Color(a, a, a, 0), [ 19997 true, 19998 true, 19999 true, 20000 true 20001 ]); 20002 } else if (this.renderContext.currentPass === "opaque") return ColorMode.unblended; 20003 else return ColorMode.alphaBlended; 20004 } 20005 getDepthModeForSublayer(n, mask, func) { 20006 if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled; 20007 const depth = 1 - ((1 + this.renderContext.currentLayer) * this.numSublayers + n) * this.depthEpsilon; 20008 return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]); 20009 } 20010 getDepthModeFor3D() { 20011 return new DepthMode(this.context.gl.LEQUAL, DepthMode.ReadWrite, this.renderContext.depthRangeFor3D); 20012 } 20013 opaquePassEnabledForLayer() { 20014 return this.renderContext.currentLayer < this.renderContext.opaquePassCutoff; 20015 } 20016 render(style, options) { 20017 this.style = style; 20018 this.options = options; 20019 const renderContext = this.renderContext = createRenderContext(this.transform, style.projection, style.map.terrain ?? null); 20020 this.lineAtlas = style.lineAtlas; 20021 this.imageManager = style.imageManager; 20022 this.patternAtlas = style.patternAtlas; 20023 this.glyphManager = style.glyphManager; 20024 this.symbolFadeChange = style.placement.symbolFadeChange(now()); 20025 updateFrameUniformBuffer(this.context.frameUniformBuffer, this); 20026 this.imageManager.beginFrame(); 20027 const layerIds = this.style._order; 20028 const tileManagers = this.style.tileManagers; 20029 const coordsAscending = {}; 20030 const coordsDescending = {}; 20031 const coordsDescendingSymbol = {}; 20032 for (const id in tileManagers) { 20033 const tileManager = tileManagers[id]; 20034 if (tileManager.used) tileManager.prepare(this.context); 20035 coordsAscending[id] = tileManager.getVisibleCoordinates(false); 20036 coordsDescending[id] = coordsAscending[id].slice().reverse(); 20037 coordsDescendingSymbol[id] = tileManager.getVisibleCoordinates(true).reverse(); 20038 } 20039 renderContext.opaquePassCutoff = Infinity; 20040 for (let i = 0; i < layerIds.length; i++) { 20041 const layerId = layerIds[i]; 20042 if (this.style._layers[layerId].is3D()) { 20043 renderContext.opaquePassCutoff = i; 20044 break; 20045 } 20046 } 20047 this.maybeDrawDepth(); 20048 if (this.renderToTexture) { 20049 this.renderToTexture.prepareForRender(this.style, this.transform.zoom); 20050 renderContext.opaquePassCutoff = 0; 20051 } 20052 renderContext.currentPass = "offscreen"; 20053 for (const layerId of layerIds) { 20054 const layer = this.style._layers[layerId]; 20055 if (!layer.hasOffscreenPass() || layer.isHidden(this.transform.zoom)) continue; 20056 const coords = coordsDescending[layer.source]; 20057 if (layer.type !== "custom" && !coords.length) continue; 20058 this.renderLayer(this, tileManagers[layer.source], layer, coords, renderContext); 20059 } 20060 this.context.viewport.set([ 20061 0, 20062 0, 20063 this.width, 20064 this.height 20065 ]); 20066 this.context.bindFramebuffer.set(null); 20067 this.context.clear({ 20068 color: options.showOverdrawInspector ? Color.black : Color.transparent, 20069 depth: 1 20070 }); 20071 this.clearStencil(); 20072 if (this.style.sky) this.drawFunctions.sky(this, this.style.sky); 20073 this._showOverdrawInspector = options.showOverdrawInspector; 20074 renderContext.depthRangeFor3D = [0, 1 - (style._order.length + 2) * this.numSublayers * this.depthEpsilon]; 20075 if (!this.renderToTexture) { 20076 renderContext.currentPass = "opaque"; 20077 for (renderContext.currentLayer = layerIds.length - 1; renderContext.currentLayer >= 0; renderContext.currentLayer--) { 20078 const layer = this.style._layers[layerIds[renderContext.currentLayer]]; 20079 if (layer.isHidden(this.transform.zoom)) continue; 20080 const tileManager = tileManagers[layer.source]; 20081 const coords = coordsAscending[layer.source]; 20082 this.renderTileClippingMasks(layer, coords); 20083 this.renderLayer(this, tileManager, layer, coords, renderContext); 20084 } 20085 } 20086 renderContext.currentPass = "translucent"; 20087 let globeDepthRendered = false; 20088 for (renderContext.currentLayer = 0; renderContext.currentLayer < layerIds.length; renderContext.currentLayer++) { 20089 const layer = this.style._layers[layerIds[renderContext.currentLayer]]; 20090 if (layer.isHidden(this.transform.zoom)) continue; 20091 const tileManager = tileManagers[layer.source]; 20092 if (this.renderToTexture?.renderLayer(layer, renderContext)) continue; 20093 if (!this.opaquePassEnabledForLayer() && !globeDepthRendered) { 20094 globeDepthRendered = true; 20095 if (renderContext.isRenderingGlobe && !this.style.map.terrain) this._renderTilesDepthBuffer(); 20096 } 20097 const coords = (layer.type === "symbol" ? coordsDescendingSymbol : coordsDescending)[layer.source]; 20098 this.renderTileClippingMasks(layer, coordsAscending[layer.source]); 20099 this.renderLayer(this, tileManager, layer, coords, renderContext); 20100 } 20101 if (renderContext.isRenderingGlobe) this.drawFunctions.atmosphere(this, this.style.sky, this.style.light); 20102 if (this.options.showTileBoundaries) { 20103 const selectedSource = selectDebugSource(this.style, this.transform.zoom); 20104 if (selectedSource) this.drawFunctions.debug(this, selectedSource, selectedSource.getVisibleCoordinates(), renderContext); 20105 } 20106 if (this.options.showPadding) this.drawFunctions.debugPadding(this); 20107 if (this.renderToTexture && !this.options.moving) this.clearRTTPool(); 20108 this.context.setDefault(); 20109 } 20110 /** 20111 * Invalidates cached terrain depth so the next eligible depth pass redraws it. 20112 * DEM data can change the rendered surface while the camera and terrain tile set stay unchanged. 20113 * Repeated calls coalesce without rendering or scheduling a frame, even if terrain is not yet present. 20114 */ 20115 markTerrainDepthDirty() { 20116 this.terrainFacilitator.depthDirty = true; 20117 } 20118 /** 20119 * Updates the depth framebuffer after explicit invalidation, camera movement, or tile reloading. 20120 */ 20121 maybeDrawDepth() { 20122 if (!this.style?.projection || !this.style.map?.terrain) return; 20123 const prevMatrix = this.terrainFacilitator.matrix; 20124 const currMatrix = this.transform.modelViewProjectionMatrix; 20125 let doUpdate = this.terrainFacilitator.depthDirty; 20126 doUpdate ||= !equals(prevMatrix, currMatrix); 20127 doUpdate ||= this.style.map.terrain.tileManager.anyTilesAfterTime(this.terrainFacilitator.renderTime); 20128 if (!doUpdate) return; 20129 copy(prevMatrix, currMatrix); 20130 this.terrainFacilitator.renderTime = now(); 20131 this.terrainFacilitator.depthDirty = false; 20132 this.drawFunctions.terrainDepth(this, this.style.map.terrain); 20133 } 20134 renderLayer(painter, tileManager, layer, coords, renderContext) { 20135 if (layer.isHidden(this.transform.zoom)) return; 20136 if (layer.type !== "background" && layer.type !== "custom" && !(coords || []).length) return; 20137 this.id = layer.id; 20138 const draw = this.drawFunctions; 20139 if (isSymbolStyleLayer(layer)) draw.symbol(painter, tileManager, layer, coords, this.style.placement.variableOffsets, renderContext); 20140 else if (isCircleStyleLayer(layer)) draw.circle(painter, tileManager, layer, coords, renderContext); 20141 else if (isHeatmapStyleLayer(layer)) draw.heatmap(painter, tileManager, layer, coords, renderContext); 20142 else if (isLineStyleLayer(layer)) draw.line(painter, tileManager, layer, coords, renderContext); 20143 else if (isFillStyleLayer(layer)) draw.fill(painter, tileManager, layer, coords, renderContext); 20144 else if (isFillExtrusionStyleLayer(layer)) draw.fillExtrusion(painter, tileManager, layer, coords, renderContext); 20145 else if (isHillshadeStyleLayer(layer)) draw.hillshade(painter, tileManager, layer, coords, renderContext); 20146 else if (isColorReliefStyleLayer(layer)) draw.colorRelief(painter, tileManager, layer, coords, renderContext); 20147 else if (isRasterStyleLayer(layer)) draw.raster(painter, tileManager, layer, coords, renderContext); 20148 else if (isBackgroundStyleLayer(layer)) draw.background(painter, tileManager, layer, coords, renderContext); 20149 else if (isCustomStyleLayer(layer)) draw.custom(painter, tileManager, layer, renderContext); 20150 } 20151 static { 20152 this.MAX_TEXTURE_POOL_SIZE_PER_BUCKET = 50; 20153 } 20154 saveTileTexture(texture) { 20155 const textures = this._tileTextures[texture.size[0]]; 20156 if (!textures) this._tileTextures[texture.size[0]] = [texture]; 20157 else if (textures.length < Painter.MAX_TEXTURE_POOL_SIZE_PER_BUCKET) textures.push(texture); 20158 else texture.destroy(); 20159 } 20160 getTileTexture(size) { 20161 const textures = this._tileTextures[size]; 20162 return textures && textures.length > 0 ? textures.pop() : null; 20163 } 20164 acquireRTT(size) { 20165 const gl = this.context.gl; 20166 const obj = this._rttObjectRecyclePool.pop(); 20167 if (obj) { 20168 if (obj.size !== size) { 20169 obj.texture.update({ 20170 width: size, 20171 height: size, 20172 data: null 20173 }, { 20174 premultiply: false, 20175 useMipmap: true 20176 }); 20177 obj.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_LINEAR); 20178 if (this.context.extTextureFilterAnisotropic) gl.texParameterf(gl.TEXTURE_2D, this.context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, this.context.extTextureFilterAnisotropicMax); 20179 obj.size = size; 20180 } 20181 return obj; 20182 } 20183 const texture = new Texture(this.context, { 20184 width: size, 20185 height: size, 20186 data: null 20187 }, gl.RGBA, { 20188 premultiply: false, 20189 useMipmap: true 20190 }); 20191 texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_LINEAR); 20192 if (this.context.extTextureFilterAnisotropic) gl.texParameterf(gl.TEXTURE_2D, this.context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, this.context.extTextureFilterAnisotropicMax); 20193 return { 20194 texture, 20195 size 20196 }; 20197 } 20198 /** 20199 * Binds the shared RTT FBO with the given RTTObject's texture attached 20200 * as color target. Creates / resizes the shared FBO and depth-stencil 20201 * renderbuffer lazily. 20202 */ 20203 bindRTT(obj) { 20204 const gl = this.context.gl; 20205 const size = obj.size; 20206 if (!this._rttSharedFbo) { 20207 const fbo = this.context.createFramebuffer(size, size, true, true); 20208 const depthRenderbuffer = this.context.createRenderbuffer(gl.DEPTH_STENCIL, size, size); 20209 fbo.depthAttachment.set(depthRenderbuffer); 20210 this._rttSharedFbo = { 20211 fbo, 20212 depthRenderbuffer, 20213 size 20214 }; 20215 } 20216 if (this._rttSharedFbo.size !== size) { 20217 this.context.bindRenderbuffer.set(this._rttSharedFbo.depthRenderbuffer); 20218 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, size, size); 20219 this.context.bindRenderbuffer.set(null); 20220 this._rttSharedFbo.fbo.width = size; 20221 this._rttSharedFbo.fbo.height = size; 20222 this._rttSharedFbo.size = size; 20223 } 20224 this._rttSharedFbo.fbo.colorAttachment.set(obj.texture.texture); 20225 this.context.bindFramebuffer.set(this._rttSharedFbo.fbo.framebuffer); 20226 } 20227 releaseRTT(obj) { 20228 this._rttObjectRecyclePool.push(obj); 20229 } 20230 /** 20231 * Destroys the pooled {@link RTTObject}s, the drapes no tile holds. Called at the end of a frame at rest, 20232 * when they would otherwise stay resident until the next gesture reuses them. 20233 */ 20234 clearRTTPool() { 20235 for (const obj of this._rttObjectRecyclePool) obj.texture.destroy(); 20236 this._rttObjectRecyclePool.length = 0; 20237 } 20238 /** 20239 * Frees the pooled drapes and the shared render-to-texture FBO. Called when terrain is removed, after its tiles 20240 * released their drapes. 20241 */ 20242 destroyRTTResources() { 20243 this.clearRTTPool(); 20244 if (!this._rttSharedFbo) return; 20245 this._rttSharedFbo.fbo.colorAttachment.set(null); 20246 this._rttSharedFbo.fbo.depthAttachment.set(null); 20247 const gl = this.context.gl; 20248 gl.deleteRenderbuffer(this._rttSharedFbo.depthRenderbuffer); 20249 gl.deleteFramebuffer(this._rttSharedFbo.fbo.framebuffer); 20250 this._rttSharedFbo = null; 20251 } 20252 /** 20253 * Checks whether a pattern image is needed, and if it is, whether it is not loaded. 20254 * 20255 * @returns true if a needed image is missing and rendering needs to be skipped. 20256 */ 20257 isPatternMissing(image) { 20258 if (!image) return false; 20259 if (!image.from || !image.to) return true; 20260 const imagePosA = this.patternAtlas.getPattern(image.from.toString()); 20261 const imagePosB = this.patternAtlas.getPattern(image.to.toString()); 20262 return !imagePosA || !imagePosB; 20263 } 20264 /** 20265 * Finds the required shader and its variant (base/terrain/globe, etc.) and binds it, compiling a new shader if required. 20266 * @param name - Name of the desired shader. 20267 * @param programConfiguration - Configuration of shader's inputs. 20268 * @param forceSimpleProjection - Whether to force the use of a shader variant with simple mercator projection vertex shader. 20269 * @param defines - Additional macros to be injected at the beginning of the shader. Expected format is `['#define XYZ']`, etc. 20270 * False by default. Use true when drawing with a simple projection matrix is desired, eg. when drawing a fullscreen quad. 20271 * @returns 20272 */ 20273 useProgram(name, programConfiguration, forceSimpleProjection = false, defines = []) { 20274 this.cache ||= {}; 20275 const useTerrain = !!this.style.map.terrain; 20276 const projection = this.style.projection; 20277 const projectionPrelude = forceSimpleProjection ? shaders.projectionMercator : projection.shaderPreludeCode; 20278 const projectionDefine = forceSimpleProjection ? MercatorShaderDefine : projection.shaderDefine; 20279 const projectionKey = `/${forceSimpleProjection ? MercatorShaderVariantKey : projection.shaderVariantName}`; 20280 const configurationKey = programConfiguration ? programConfiguration.cacheKey : ""; 20281 const overdrawKey = this._showOverdrawInspector ? "/overdraw" : ""; 20282 const terrainKey = useTerrain ? "/terrain" : ""; 20283 const definesKey = defines ? `/${defines.join("/")}` : ""; 20284 const key = name + configurationKey + projectionKey + overdrawKey + terrainKey + definesKey; 20285 this.cache[key] ||= new Program(this.context, shaders[name], programConfiguration, programUniforms[name], this._showOverdrawInspector, useTerrain, projectionPrelude, projectionDefine, defines); 20286 return this.cache[key]; 20287 } 20288 setCustomLayerDefaults() { 20289 this.context.setCustomLayerDefaults(); 20290 } 20291 setBaseState() { 20292 const gl = this.context.gl; 20293 this.context.cullFace.set(false); 20294 this.context.viewport.set([ 20295 0, 20296 0, 20297 this.width, 20298 this.height 20299 ]); 20300 this.context.blendEquation.set(gl.FUNC_ADD); 20301 } 20302 initDebugOverlayCanvas() { 20303 if (this.debugOverlayCanvas == null) { 20304 this.debugOverlayCanvas = document.createElement("canvas"); 20305 this.debugOverlayCanvas.width = 512; 20306 this.debugOverlayCanvas.height = 512; 20307 const gl = this.context.gl; 20308 this.debugOverlayTexture = new Texture(this.context, this.debugOverlayCanvas, gl.RGBA); 20309 } 20310 } 20311 destroy() { 20312 if (this._tileTextures) { 20313 for (const size in this._tileTextures) { 20314 const textures = this._tileTextures[size]; 20315 if (textures) for (const texture of textures) texture.destroy(); 20316 } 20317 this._tileTextures = {}; 20318 } 20319 this.destroyRTTResources(); 20320 this.layerOpacityFbo?.destroy(); 20321 this.layerOpacityFbo = null; 20322 if (this.tileExtentBuffer) this.tileExtentBuffer.destroy(); 20323 if (this.debugBuffer) this.debugBuffer.destroy(); 20324 if (this.rasterBoundsBuffer) this.rasterBoundsBuffer.destroy(); 20325 if (this.rasterBoundsBufferPosOnly) this.rasterBoundsBufferPosOnly.destroy(); 20326 if (this.viewportBuffer) this.viewportBuffer.destroy(); 20327 if (this.tileBorderIndexBuffer) this.tileBorderIndexBuffer.destroy(); 20328 if (this.quadTriangleIndexBuffer) this.quadTriangleIndexBuffer.destroy(); 20329 if (this.tileExtentMesh) this.tileExtentMesh.vertexBuffer?.destroy(); 20330 if (this.tileExtentMesh) this.tileExtentMesh.indexBuffer?.destroy(); 20331 if (this.debugOverlayTexture) this.debugOverlayTexture.destroy(); 20332 this.context.projectionUniformBuffer.destroy(); 20333 this.context.terrainUniformBuffer.destroy(); 20334 this.context.frameUniformBuffer.destroy(); 20335 if (this.cache) { 20336 for (const key in this.cache) { 20337 const program = this.cache[key]; 20338 if (program?.program) this.context.gl.deleteProgram(program.program); 20339 } 20340 this.cache = {}; 20341 } 20342 if (this.context) this.context.setDefault(); 20343 } 20344 overLimit() { 20345 const { drawingBufferWidth, drawingBufferHeight } = this.context.gl; 20346 return this.width !== drawingBufferWidth || this.height !== drawingBufferHeight; 20347 } 20348}; 20349//#endregion 20350//#region src/util/gpu_initialization_error.ts 20351/** 20352* Thrown by the `Map` constructor when a GPU rendering context cannot be created, 20353* or fired via the map's `error` event when recreating the context after a `webglcontextrestored` fails. 20354* 20355* Carries the canvas attributes that were requested and, when the browser provided one, 20356* the originating `webglcontextcreationerror` `statusMessage`. 20357* Consumers can branch on `instanceof GPUInitializationError` and inspect the cause programmatically. 20358* @see https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL 20359*/ 20360var GPUInitializationError = class extends Error { 20361 constructor(requestedAttributes, creationEvent) { 20362 super("WebGL2 is required to display this map. We are sorry, but it seems that your browser does not support WebGL2, a technology for rendering 3D graphics on the web. Read more on https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL"); 20363 this.name = "GPUInitializationError"; 20364 this.requestedAttributes = requestedAttributes; 20365 this.statusMessage = creationEvent?.statusMessage ?? null; 20366 } 20367}; 20368//#endregion 20369//#region src/util/throttle.ts 20370/** 20371* Throttle the given function to run at most every `period` milliseconds. 20372*/ 20373function throttle(fn, time) { 20374 let pending = false; 20375 let timerId = null; 20376 let lastCallArgs; 20377 const later = () => { 20378 timerId = null; 20379 if (pending) { 20380 fn(...lastCallArgs); 20381 timerId = setTimeout(later, time); 20382 pending = false; 20383 } 20384 }; 20385 return (...args) => { 20386 pending = true; 20387 lastCallArgs = args; 20388 if (!timerId) later(); 20389 return timerId; 20390 }; 20391} 20392//#endregion 20393//#region src/ui/hash.ts 20394/** 20395* Adds the map's position to its page's location hash. 20396* Passed as an option to the map object. 20397* 20398* @group Markers and Controls 20399*/ 20400var Hash = class { 20401 constructor(hashName) { 20402 this._getHashParams = () => { 20403 return new URLSearchParams(window.location.hash.replace("#", "")); 20404 }; 20405 this._getCurrentHash = () => { 20406 const params = this._getHashParams(); 20407 if (this._hashName) return (params.get(this._hashName) || "").split("/"); 20408 return ([...params.keys()][0] ?? "").split("/"); 20409 }; 20410 this._onHashChange = () => { 20411 const hash = this._getCurrentHash(); 20412 if (!this._isValidHash(hash)) return false; 20413 const bearing = this._map.dragRotate.isEnabled() && this._map.touchZoomRotate.isEnabled() ? +(hash[3] || 0) : this._map.getBearing(); 20414 this._map.jumpTo({ 20415 center: [+hash[2], +hash[1]], 20416 zoom: +hash[0], 20417 bearing, 20418 pitch: +(hash[4] || 0) 20419 }); 20420 return true; 20421 }; 20422 this._updateHashUnthrottled = () => { 20423 const location = window.location.href.replace(/(#.*)?$/, this.getHashString()); 20424 window.history.replaceState(window.history.state, null, location); 20425 }; 20426 this._removeHash = () => { 20427 const params = this._getHashParams(); 20428 if (this._hashName) params.delete(this._hashName); 20429 else { 20430 const keys = Array.from(params.keys()); 20431 if (keys.length > 0) params.delete(keys[0]); 20432 } 20433 const newHash = decodeURIComponent(params.toString()).replace(/=&/g, "&").replace(/=$/g, ""); 20434 const replaceString = newHash ? `#${newHash}` : ""; 20435 let location = window.location.href.replace(/(#.+)?$/, replaceString); 20436 location = location.replace("&&", "&"); 20437 window.history.replaceState(window.history.state, null, location); 20438 }; 20439 this._updateHash = throttle(this._updateHashUnthrottled, 300); 20440 this._hashName = hashName && encodeURIComponent(hashName); 20441 } 20442 /** 20443 * Map element to listen for coordinate changes 20444 * 20445 * @param map - The map object 20446 */ 20447 addTo(map) { 20448 this._map = map; 20449 addEventListener("hashchange", this._onHashChange, false); 20450 this._map.on("moveend", this._updateHash); 20451 return this; 20452 } 20453 /** 20454 * Removes hash 20455 */ 20456 remove() { 20457 removeEventListener("hashchange", this._onHashChange, false); 20458 this._map.off("moveend", this._updateHash); 20459 clearTimeout(this._updateHash()); 20460 this._removeHash(); 20461 delete this._map; 20462 return this; 20463 } 20464 getHashString(mapFeedback) { 20465 const center = this._map.getCenter(), zoom = Math.round(this._map.getZoom() * 100) / 100, precision = Math.ceil((zoom * Math.LN2 + Math.log(512 / 360 / .5)) / Math.LN10), m = Math.pow(10, precision), lng = Math.round(center.lng * m) / m, lat = Math.round(center.lat * m) / m, bearing = this._map.getBearing(), pitch = this._map.getPitch(); 20466 let hash = ""; 20467 if (mapFeedback) hash += `/${lng}/${lat}/${zoom}`; 20468 else hash += `${zoom}/${lat}/${lng}`; 20469 if (bearing || pitch) hash += `/${Math.round(bearing * 10) / 10}`; 20470 if (pitch) hash += `/${Math.round(pitch)}`; 20471 if (this._hashName) { 20472 const params = this._getHashParams(); 20473 params.set(this._hashName, hash); 20474 return `#${decodeURIComponent(params.toString()).replace(/=&/g, "&").replace(/=$/g, "")}`; 20475 } 20476 return `#${hash}`; 20477 } 20478 _isValidHash(hash) { 20479 if (hash.length < 3 || hash.some((h) => isNaN(+h))) return false; 20480 try { 20481 new LngLat(+hash[2], +hash[1]); 20482 } catch { 20483 return false; 20484 } 20485 const zoom = +hash[0]; 20486 const bearing = +(hash[3] || 0); 20487 const pitch = +(hash[4] || 0); 20488 return zoom >= this._map.getMinZoom() && zoom <= this._map.getMaxZoom() && bearing >= -180 && bearing <= 180 && pitch >= this._map.getMinPitch() && pitch <= this._map.getMaxPitch(); 20489 } 20490}; 20491//#endregion 20492//#region src/ui/handler_inertia.ts 20493const defaultInertiaOptions = { 20494 linearity: .3, 20495 easing: bezier(0, 0, .3, 1) 20496}; 20497const defaultPanInertiaOptions = extend({ 20498 deceleration: 2500, 20499 maxSpeed: 1400 20500}, defaultInertiaOptions); 20501const defaultZoomInertiaOptions = extend({ 20502 deceleration: 20, 20503 maxSpeed: 1400 20504}, defaultInertiaOptions); 20505const defaultBearingInertiaOptions = extend({ 20506 deceleration: 1e3, 20507 maxSpeed: 360 20508}, defaultInertiaOptions); 20509const defaultPitchInertiaOptions = extend({ 20510 deceleration: 1e3, 20511 maxSpeed: 90 20512}, defaultInertiaOptions); 20513const defaultRollInertiaOptions = extend({ 20514 deceleration: 1e3, 20515 maxSpeed: 360 20516}, defaultInertiaOptions); 20517/** 20518* The maximum age of a recorded gesture update, in milliseconds. 20519*/ 20520const BUFFER_CUTOFF = 160; 20521/** 20522* The time window the gesture velocity is measured over, in milliseconds. 20523*/ 20524const VELOCITY_WINDOW = 60; 20525var HandlerInertia = class { 20526 constructor(map) { 20527 this._map = map; 20528 this.clear(); 20529 } 20530 clear() { 20531 this._inertiaBuffer = []; 20532 } 20533 record(settings) { 20534 this._drainInertiaBuffer(); 20535 this._inertiaBuffer.push({ 20536 time: now(), 20537 settings 20538 }); 20539 } 20540 _drainInertiaBuffer() { 20541 const inertia = this._inertiaBuffer, currentTime = now(); 20542 while (inertia.length > 0 && currentTime - inertia[0].time > BUFFER_CUTOFF) inertia.shift(); 20543 } 20544 /** 20545 * Returns the entries the velocity is measured from: everything recorded within 20546 * {@link VELOCITY_WINDOW} before now, but never fewer than the last two, so that 20547 * inertia also works below two recorded updates per window. 20548 */ 20549 _getVelocityEntries() { 20550 const inertia = this._inertiaBuffer; 20551 const windowStart = now() - VELOCITY_WINDOW; 20552 let firstIndex = Math.max(0, inertia.length - 2); 20553 while (firstIndex > 0 && inertia[firstIndex - 1].time >= windowStart) firstIndex--; 20554 return inertia.slice(firstIndex); 20555 } 20556 /** 20557 * Returns the ease which continues the gesture that has just ended. 20558 * 20559 * The velocity is measured over the time up to the release and not up to the last 20560 * recorded movement, so that a gesture held still before being released has a lower 20561 * velocity, down to no inertia at all. The delta of an entry happened before it was 20562 * recorded, so the first entry only marks the start of the measured interval; anchors 20563 * are the exception, as they describe the state at the moment of their entry. 20564 * 20565 * @param panInertiaOptions - overrides for the pan inertia defaults 20566 * @returns the ease options, or `undefined` when the measured interval holds no 20567 * movement, which terrain gestures produce by recording updates without any delta 20568 */ 20569 _onMoveEnd(panInertiaOptions) { 20570 this._drainInertiaBuffer(); 20571 const entries = this._getVelocityEntries(); 20572 if (entries.length < 2) { 20573 this.clear(); 20574 return; 20575 } 20576 const deltas = { 20577 zoom: 0, 20578 bearing: 0, 20579 pitch: 0, 20580 roll: 0, 20581 pan: new Point(0, 0), 20582 pinchAround: void 0, 20583 around: void 0 20584 }; 20585 for (const { settings } of entries) { 20586 if (settings.around) deltas.around = settings.around; 20587 if (settings.pinchAround) deltas.pinchAround = settings.pinchAround; 20588 } 20589 for (const { settings } of entries.slice(1)) { 20590 deltas.zoom += settings.zoomDelta || 0; 20591 deltas.bearing += settings.bearingDelta || 0; 20592 deltas.pitch += settings.pitchDelta || 0; 20593 deltas.roll += settings.rollDelta || 0; 20594 if (settings.panDelta) deltas.pan._add(settings.panDelta); 20595 } 20596 if (!deltas.pan.mag() && !deltas.zoom && !deltas.bearing && !deltas.pitch && !deltas.roll) { 20597 this.clear(); 20598 return; 20599 } 20600 const duration = now() - entries[0].time; 20601 const easeOptions = {}; 20602 if (deltas.pan.mag()) { 20603 const result = calculateEasing(deltas.pan.mag(), duration, extend({}, defaultPanInertiaOptions, panInertiaOptions || {})); 20604 const finalPan = deltas.pan.mult(result.amount / deltas.pan.mag()); 20605 const computedEaseOptions = this._map._camera.cameraHelper.handlePanInertia(finalPan, this._map._camera.transform); 20606 easeOptions.center = computedEaseOptions.easingCenter; 20607 easeOptions.offset = computedEaseOptions.easingOffset; 20608 extendDuration(easeOptions, result); 20609 } 20610 if (deltas.zoom) { 20611 const result = calculateEasing(deltas.zoom, duration, defaultZoomInertiaOptions); 20612 easeOptions.zoom = evaluateZoomSnap(this._map.getZoom() + result.amount, this._map.getZoomSnap(), result.amount); 20613 extendDuration(easeOptions, result); 20614 } 20615 if (deltas.bearing) { 20616 const result = calculateEasing(deltas.bearing, duration, defaultBearingInertiaOptions); 20617 easeOptions.bearing = this._map.getBearing() + clamp(result.amount, -179, 179); 20618 extendDuration(easeOptions, result); 20619 } 20620 if (deltas.pitch) { 20621 const result = calculateEasing(deltas.pitch, duration, defaultPitchInertiaOptions); 20622 easeOptions.pitch = this._map.getPitch() + result.amount; 20623 extendDuration(easeOptions, result); 20624 } 20625 if (deltas.roll) { 20626 const result = calculateEasing(deltas.roll, duration, defaultRollInertiaOptions); 20627 easeOptions.roll = this._map.getRoll() + clamp(result.amount, -179, 179); 20628 extendDuration(easeOptions, result); 20629 } 20630 if (easeOptions.zoom || easeOptions.bearing) { 20631 const last = deltas.pinchAround === void 0 ? deltas.around : deltas.pinchAround; 20632 easeOptions.around = last ? this._map.unproject(last) : this._map.getCenter(); 20633 } 20634 this.clear(); 20635 return extend(easeOptions, { noMoveStart: true }); 20636 } 20637}; 20638function extendDuration(easeOptions, result) { 20639 if (!easeOptions.duration || easeOptions.duration < result.duration) { 20640 easeOptions.duration = result.duration; 20641 easeOptions.easing = result.easing; 20642 } 20643} 20644function calculateEasing(amount, inertiaDuration, inertiaOptions) { 20645 const { maxSpeed, linearity, deceleration } = inertiaOptions; 20646 const speed = clamp(amount * linearity / (inertiaDuration / 1e3), -maxSpeed, maxSpeed); 20647 const duration = Math.abs(speed) / (deceleration * linearity); 20648 return { 20649 easing: inertiaOptions.easing, 20650 duration: duration * 1e3, 20651 amount: speed * (duration / 2) 20652 }; 20653} 20654//#endregion 20655//#region src/ui/handler/map_event.ts 20656var MapEventHandler = class { 20657 constructor(map, options) { 20658 this._map = map; 20659 this._clickTolerance = options.clickTolerance; 20660 } 20661 reset() { 20662 delete this._mousedownPos; 20663 } 20664 wheel(e) { 20665 return this._firePreventable(new MapWheelEvent(this._map, e)); 20666 } 20667 mousedown(e, point) { 20668 this._mousedownPos = point; 20669 return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); 20670 } 20671 mouseup(e) { 20672 this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20673 } 20674 click(e, point) { 20675 if (this._mousedownPos && this._mousedownPos.dist(point) >= this._clickTolerance) return; 20676 this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20677 } 20678 dblclick(e) { 20679 return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); 20680 } 20681 mouseover(e) { 20682 this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20683 } 20684 mouseout(e) { 20685 this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20686 } 20687 touchstart(e) { 20688 return this._firePreventable(new MapTouchEvent(e.type, this._map, e)); 20689 } 20690 touchmove(e) { 20691 this._map.fire(new MapTouchEvent(e.type, this._map, e)); 20692 } 20693 touchend(e) { 20694 this._map.fire(new MapTouchEvent(e.type, this._map, e)); 20695 } 20696 touchcancel(e) { 20697 this._map.fire(new MapTouchEvent(e.type, this._map, e)); 20698 } 20699 _firePreventable(mapEvent) { 20700 this._map.fire(mapEvent); 20701 if (mapEvent.defaultPrevented) return {}; 20702 } 20703 isEnabled() { 20704 return true; 20705 } 20706 isActive() { 20707 return false; 20708 } 20709 enable() {} 20710 disable() {} 20711}; 20712/** 20713* A single finger held in place this long fires a long-press contextmenu. 20714*/ 20715const LONG_PRESS_DELAY = 500; 20716var BlockableMapEventHandler = class { 20717 constructor(map, options) { 20718 this._map = map; 20719 this._clickTolerance = options.clickTolerance; 20720 } 20721 reset() { 20722 this._delayContextMenu = false; 20723 this._ignoreContextMenu = true; 20724 delete this._contextMenuEvent; 20725 this._clearLongPress(); 20726 this._touchActive = false; 20727 } 20728 mousemove(e) { 20729 this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20730 } 20731 mousedown() { 20732 this._delayContextMenu = true; 20733 this._ignoreContextMenu = false; 20734 } 20735 mouseup() { 20736 this._delayContextMenu = false; 20737 if (this._contextMenuEvent) { 20738 this._map.fire(new MapMouseEvent("contextmenu", this._map, this._contextMenuEvent)); 20739 delete this._contextMenuEvent; 20740 } 20741 } 20742 /** 20743 * Starts the touch long press. Touch devices have no right click, so a long press opens 20744 * the context menu. It is detected with a timer rather than by relying on a native 20745 * contextmenu event, because Android Chrome fires one on a long press but iOS 20746 * Safari does not. A single finger held past the delay without moving fires a 20747 * contextmenu at that point; a pan or a lift cancels it. 20748 * @param e - the touch event 20749 * @param points - the touch points, in map coordinates 20750 * @param mapTouches - the touches that are on the map 20751 */ 20752 touchstart(e, points, mapTouches) { 20753 this._clearLongPress(); 20754 this._touchActive = mapTouches.length > 0; 20755 if (mapTouches.length !== 1) return; 20756 this._longPressStart = points[0]; 20757 const touch = mapTouches[0]; 20758 this._longPressTimer = setTimeout(() => { 20759 this._longPressTimer = void 0; 20760 const originalEvent = new MouseEvent("contextmenu", { 20761 clientX: touch.clientX, 20762 clientY: touch.clientY, 20763 button: 2, 20764 bubbles: true, 20765 cancelable: true 20766 }); 20767 this._map.fire(new MapMouseEvent("contextmenu", this._map, originalEvent)); 20768 }, LONG_PRESS_DELAY); 20769 } 20770 /** 20771 * Cancels a pending long press once the finger has moved further than the map's 20772 * click tolerance, or once a second finger lands. 20773 * @param e - the touch event 20774 * @param points - the touch points, in map coordinates 20775 * @param mapTouches - the touches that are on the map 20776 */ 20777 touchmove(e, points, mapTouches) { 20778 if (!this._longPressTimer) return; 20779 if (mapTouches.length !== 1 || !this._longPressStart || points[0].dist(this._longPressStart) > this._clickTolerance) this._clearLongPress(); 20780 } 20781 touchend() { 20782 this._touchActive = false; 20783 this._clearLongPress(); 20784 } 20785 touchcancel() { 20786 this._touchActive = false; 20787 this._clearLongPress(); 20788 } 20789 _clearLongPress() { 20790 if (this._longPressTimer) { 20791 clearTimeout(this._longPressTimer); 20792 this._longPressTimer = void 0; 20793 } 20794 this._longPressStart = void 0; 20795 } 20796 /** 20797 * Handles a contextmenu event. A native one during a touch is suppressed, because the 20798 * long press timer already fires the event and the browser menu would double up. 20799 * @param e - the mouse event 20800 */ 20801 contextmenu(e) { 20802 if (this._touchActive) { 20803 e.preventDefault(); 20804 return; 20805 } 20806 if (this._delayContextMenu) this._contextMenuEvent = e; 20807 else if (!this._ignoreContextMenu) this._map.fire(new MapMouseEvent(e.type, this._map, e)); 20808 if (this._map.listens("contextmenu")) e.preventDefault(); 20809 } 20810 isEnabled() { 20811 return true; 20812 } 20813 isActive() { 20814 return false; 20815 } 20816 enable() {} 20817 disable() {} 20818}; 20819//#endregion 20820//#region src/ui/handler/box_zoom.ts 20821/** 20822* The `BoxZoomHandler` allows the user to zoom the map to fit within a bounding box. 20823* The bounding box is defined by clicking and holding `shift` while dragging the cursor. 20824* 20825* @group Handlers 20826*/ 20827var BoxZoomHandler = class { 20828 /** @internal */ 20829 constructor(map, options, transformProvider) { 20830 this._map = map; 20831 this._tr = transformProvider; 20832 this._el = map.getCanvasContainer(); 20833 this._container = map.getContainer(); 20834 this._clickTolerance = options.clickTolerance || 1; 20835 if (options.boxZoom && typeof options.boxZoom === "object") this._boxZoomEnd = options.boxZoom.boxZoomEnd; 20836 } 20837 /** 20838 * Returns a Boolean indicating whether the "box zoom" interaction is enabled. 20839 * 20840 * @returns `true` if the "box zoom" interaction is enabled. 20841 */ 20842 isEnabled() { 20843 return !!this._enabled; 20844 } 20845 /** 20846 * Returns a Boolean indicating whether the "box zoom" interaction is active, i.e. currently being used. 20847 * 20848 * @returns `true` if the "box zoom" interaction is active. 20849 */ 20850 isActive() { 20851 return !!this._active; 20852 } 20853 /** 20854 * Enables the "box zoom" interaction. 20855 * 20856 * @example 20857 * ```ts 20858 * map.boxZoom.enable(); 20859 * ``` 20860 */ 20861 enable() { 20862 if (this.isEnabled()) return; 20863 this._enabled = true; 20864 } 20865 /** 20866 * Disables the "box zoom" interaction. 20867 * 20868 * @example 20869 * ```ts 20870 * map.boxZoom.disable(); 20871 * ``` 20872 */ 20873 disable() { 20874 if (!this.isEnabled()) return; 20875 this._enabled = false; 20876 } 20877 mousedown(e, point) { 20878 if (!this.isEnabled()) return; 20879 if (!(e.shiftKey && e.button === 0)) return; 20880 DOM.disableDrag(); 20881 this._startPos = this._lastPos = point; 20882 this._active = true; 20883 } 20884 mousemoveWindow(e, point) { 20885 if (!this._active) return; 20886 const pos = point; 20887 if (this._lastPos.equals(pos) || !this._box && pos.dist(this._startPos) < this._clickTolerance) return; 20888 const p0 = this._startPos; 20889 this._lastPos = pos; 20890 if (!this._box) { 20891 this._box = DOM.create("div", "maplibregl-boxzoom", this._container); 20892 this._container.classList.add("maplibregl-crosshair"); 20893 this._fireEvent("boxzoomstart", e); 20894 } 20895 const minX = Math.min(p0.x, pos.x), maxX = Math.max(p0.x, pos.x), minY = Math.min(p0.y, pos.y), maxY = Math.max(p0.y, pos.y); 20896 this._box.style.transform = `translate(${minX}px,${minY}px)`; 20897 this._box.style.width = `${maxX - minX}px`; 20898 this._box.style.height = `${maxY - minY}px`; 20899 } 20900 mouseupWindow(e, point) { 20901 if (!this._active) return; 20902 if (e.button !== 0) return; 20903 const p0 = this._startPos, p1 = point; 20904 this.reset(); 20905 DOM.suppressClick(); 20906 if (p0.x === p1.x && p0.y === p1.y) this._fireEvent("boxzoomcancel", e); 20907 else { 20908 this._map.fire(new MapBoxZoomEvent("boxzoomend", { originalEvent: e })); 20909 if (this._boxZoomEnd) { 20910 this._boxZoomEnd(this._map, p0, p1, e); 20911 return; 20912 } 20913 return { cameraAnimation: (map) => map.fitScreenCoordinates(p0, p1, this._tr.bearing, { linear: true }) }; 20914 } 20915 } 20916 keydown(e) { 20917 if (!this._active) return; 20918 if (e.keyCode === 27) { 20919 this.reset(); 20920 this._fireEvent("boxzoomcancel", e); 20921 } 20922 } 20923 reset() { 20924 this._active = false; 20925 this._container.classList.remove("maplibregl-crosshair"); 20926 if (this._box) { 20927 this._box.remove(); 20928 this._box = null; 20929 } 20930 DOM.enableDrag(); 20931 delete this._startPos; 20932 delete this._lastPos; 20933 } 20934 _fireEvent(type, e) { 20935 return this._map.fire(new MapBoxZoomEvent(type, { originalEvent: e })); 20936 } 20937}; 20938//#endregion 20939//#region src/ui/handler/handler_util.ts 20940function indexTouches(touches, points) { 20941 if (touches.length !== points.length) throw new Error(`The number of touches and points are not equal - touches ${touches.length}, points ${points.length}`); 20942 const obj = {}; 20943 for (let i = 0; i < touches.length; i++) obj[touches[i].identifier] = points[i]; 20944 return obj; 20945} 20946//#endregion 20947//#region src/ui/handler/tap_recognizer.ts 20948function getCentroid(points) { 20949 const sum = new Point(0, 0); 20950 for (const point of points) sum._add(point); 20951 return sum.div(points.length); 20952} 20953const MAX_TOUCH_TIME = 500; 20954var SingleTapRecognizer = class { 20955 constructor(options) { 20956 this.reset(); 20957 this.numTouches = options.numTouches; 20958 } 20959 reset() { 20960 delete this.centroid; 20961 delete this.startTime; 20962 delete this.touches; 20963 this.aborted = false; 20964 } 20965 touchstart(e, points, mapTouches) { 20966 if (this.centroid || mapTouches.length > this.numTouches) this.aborted = true; 20967 if (this.aborted) return; 20968 if (this.startTime === void 0) this.startTime = e.timeStamp; 20969 if (mapTouches.length === this.numTouches) { 20970 this.centroid = getCentroid(points); 20971 this.touches = indexTouches(mapTouches, points); 20972 } 20973 } 20974 touchmove(e, points, mapTouches) { 20975 if (this.aborted || !this.centroid) return; 20976 const newTouches = indexTouches(mapTouches, points); 20977 for (const id in this.touches) { 20978 const prevPos = this.touches[id]; 20979 const pos = newTouches[id]; 20980 if (!pos || pos.dist(prevPos) > 30) this.aborted = true; 20981 } 20982 } 20983 touchend(e, points, mapTouches) { 20984 if (!this.centroid || e.timeStamp - this.startTime > MAX_TOUCH_TIME) this.aborted = true; 20985 if (mapTouches.length === 0) { 20986 const centroid = !this.aborted && this.centroid; 20987 this.reset(); 20988 if (centroid) return centroid; 20989 } 20990 } 20991}; 20992var TapRecognizer = class { 20993 constructor(options) { 20994 this.singleTap = new SingleTapRecognizer(options); 20995 this.numTaps = options.numTaps; 20996 this.reset(); 20997 } 20998 reset() { 20999 this.lastTime = Infinity; 21000 delete this.lastTap; 21001 this.count = 0; 21002 this.singleTap.reset(); 21003 } 21004 touchstart(e, points, mapTouches) { 21005 this.singleTap.touchstart(e, points, mapTouches); 21006 } 21007 touchmove(e, points, mapTouches) { 21008 this.singleTap.touchmove(e, points, mapTouches); 21009 } 21010 touchend(e, points, mapTouches) { 21011 const tap = this.singleTap.touchend(e, points, mapTouches); 21012 if (tap) { 21013 const soonEnough = e.timeStamp - this.lastTime < 500; 21014 const closeEnough = !this.lastTap || this.lastTap.dist(tap) < 30; 21015 if (!soonEnough || !closeEnough) this.reset(); 21016 this.count++; 21017 this.lastTime = e.timeStamp; 21018 this.lastTap = tap; 21019 if (this.count === this.numTaps) { 21020 this.reset(); 21021 return tap; 21022 } 21023 } 21024 } 21025}; 21026//#endregion 21027//#region src/ui/handler/tap_zoom.ts 21028/** 21029* A `TapZoomHandler` allows the user to zoom the map at a point by double tapping 21030*/ 21031var TapZoomHandler = class { 21032 constructor(map, transformProvider) { 21033 this._tr = transformProvider; 21034 this._zoomIn = new TapRecognizer({ 21035 numTouches: 1, 21036 numTaps: 2 21037 }); 21038 this._zoomOut = new TapRecognizer({ 21039 numTouches: 2, 21040 numTaps: 1 21041 }); 21042 this.reset(); 21043 } 21044 reset() { 21045 this._active = false; 21046 this._zoomIn.reset(); 21047 this._zoomOut.reset(); 21048 } 21049 touchstart(e, points, mapTouches) { 21050 this._zoomIn.touchstart(e, points, mapTouches); 21051 this._zoomOut.touchstart(e, points, mapTouches); 21052 } 21053 touchmove(e, points, mapTouches) { 21054 this._zoomIn.touchmove(e, points, mapTouches); 21055 this._zoomOut.touchmove(e, points, mapTouches); 21056 } 21057 touchend(e, points, mapTouches) { 21058 const zoomInPoint = this._zoomIn.touchend(e, points, mapTouches); 21059 const zoomOutPoint = this._zoomOut.touchend(e, points, mapTouches); 21060 const tr = this._tr; 21061 if (zoomInPoint) { 21062 this._active = true; 21063 e.preventDefault(); 21064 setTimeout(() => this.reset(), 0); 21065 return { cameraAnimation: (map) => map.easeTo({ 21066 duration: 300, 21067 zoom: evaluateZoomSnap(tr.zoom + 1, map.getZoomSnap()), 21068 around: tr.unproject(zoomInPoint) 21069 }, { originalEvent: e }) }; 21070 } else if (zoomOutPoint) { 21071 this._active = true; 21072 e.preventDefault(); 21073 setTimeout(() => this.reset(), 0); 21074 return { cameraAnimation: (map) => map.easeTo({ 21075 duration: 300, 21076 zoom: evaluateZoomSnap(tr.zoom - 1, map.getZoomSnap()), 21077 around: tr.unproject(zoomOutPoint) 21078 }, { originalEvent: e }) }; 21079 } 21080 } 21081 touchcancel() { 21082 this.reset(); 21083 } 21084 enable() { 21085 this._enabled = true; 21086 } 21087 disable() { 21088 this._enabled = false; 21089 this.reset(); 21090 } 21091 isEnabled() { 21092 return this._enabled; 21093 } 21094 isActive() { 21095 return this._active; 21096 } 21097}; 21098//#endregion 21099//#region src/ui/handler/drag_handler.ts 21100/** 21101* A generic class to create handlers for drag events, from both mouse and touch events. 21102*/ 21103var DragHandler = class { 21104 constructor(options) { 21105 this._enabled = !!options.enable; 21106 this._moveStateManager = options.moveStateManager; 21107 this._clickTolerance = options.clickTolerance || 1; 21108 this._moveFunction = options.move; 21109 this._activateOnStart = !!options.activateOnStart; 21110 options.assignEvents(this); 21111 this.reset(); 21112 } 21113 reset(e) { 21114 this._active = false; 21115 this._moved = false; 21116 delete this._lastPoint; 21117 this._moveStateManager.endMove(e); 21118 } 21119 _move(...params) { 21120 const move = this._moveFunction(...params); 21121 if (move.bearingDelta || move.pitchDelta || move.rollDelta || move.around || move.panDelta) { 21122 this._active = true; 21123 return move; 21124 } 21125 } 21126 dragStart(e, point) { 21127 if (!this.isEnabled() || this._lastPoint) return; 21128 if (!this._moveStateManager.isValidStartEvent(e)) return; 21129 this._moveStateManager.startMove(e); 21130 this._lastPoint = Array.isArray(point) ? point[0] : point; 21131 if (this._activateOnStart && this._lastPoint) this._active = true; 21132 } 21133 dragMove(e, point) { 21134 if (!this.isEnabled()) return; 21135 const lastPoint = this._lastPoint; 21136 if (!lastPoint) return; 21137 e.preventDefault(); 21138 if (!this._moveStateManager.isValidMoveEvent(e)) { 21139 this.reset(e); 21140 return; 21141 } 21142 const movePoint = Array.isArray(point) ? point[0] : point; 21143 if (!this._moved && movePoint.dist(lastPoint) < this._clickTolerance) return; 21144 this._moved = true; 21145 this._lastPoint = movePoint; 21146 return this._move(lastPoint, movePoint); 21147 } 21148 dragEnd(e) { 21149 if (!this.isEnabled() || !this._lastPoint) return; 21150 if (!this._moveStateManager.isValidEndEvent(e)) return; 21151 if (this._moved) DOM.suppressClick(); 21152 this.reset(e); 21153 } 21154 enable() { 21155 this._enabled = true; 21156 } 21157 disable() { 21158 this._enabled = false; 21159 this.reset(); 21160 } 21161 isEnabled() { 21162 return this._enabled; 21163 } 21164 isActive() { 21165 return this._active; 21166 } 21167 getClickTolerance() { 21168 return this._clickTolerance; 21169 } 21170}; 21171//#endregion 21172//#region src/ui/handler/drag_move_state_manager.ts 21173const LEFT_BUTTON$1 = 0; 21174const RIGHT_BUTTON$1 = 2; 21175const BUTTONS_FLAGS = { 21176 [LEFT_BUTTON$1]: 1, 21177 [RIGHT_BUTTON$1]: 2 21178}; 21179function buttonNoLongerPressed(e, button) { 21180 const flag = BUTTONS_FLAGS[button]; 21181 return e.buttons === void 0 || (e.buttons & flag) !== flag; 21182} 21183var MouseMoveStateManager = class { 21184 constructor(options) { 21185 this._correctEvent = options.checkCorrectEvent; 21186 } 21187 startMove(e) { 21188 this._eventButton = e.button; 21189 } 21190 endMove(_e) { 21191 delete this._eventButton; 21192 } 21193 isValidStartEvent(e) { 21194 return this._correctEvent(e); 21195 } 21196 isValidMoveEvent(e) { 21197 return !buttonNoLongerPressed(e, this._eventButton); 21198 } 21199 isValidEndEvent(e) { 21200 return e.button === this._eventButton; 21201 } 21202}; 21203var OneFingerTouchMoveStateManager = class { 21204 constructor() { 21205 this._firstTouch = void 0; 21206 } 21207 _isOneFingerTouch(e) { 21208 return e.targetTouches.length === 1; 21209 } 21210 _isSameTouchEvent(e) { 21211 return e.targetTouches[0].identifier === this._firstTouch; 21212 } 21213 startMove(e) { 21214 this._firstTouch = e.targetTouches[0].identifier; 21215 } 21216 endMove(_e) { 21217 delete this._firstTouch; 21218 } 21219 isValidStartEvent(e) { 21220 return this._isOneFingerTouch(e); 21221 } 21222 isValidMoveEvent(e) { 21223 return this._isOneFingerTouch(e) && this._isSameTouchEvent(e); 21224 } 21225 isValidEndEvent(e) { 21226 return this._isOneFingerTouch(e) && this._isSameTouchEvent(e); 21227 } 21228}; 21229var MouseOrTouchMoveStateManager = class { 21230 constructor(mouseMoveStateManager = new MouseMoveStateManager({ checkCorrectEvent: () => true }), oneFingerTouchMoveStateManager = new OneFingerTouchMoveStateManager()) { 21231 this.mouseMoveStateManager = mouseMoveStateManager; 21232 this.oneFingerTouchMoveStateManager = oneFingerTouchMoveStateManager; 21233 } 21234 _executeRelevantHandler(e, onMouseEvent, onTouchEvent) { 21235 if (e instanceof MouseEvent) return onMouseEvent(e); 21236 if (typeof TouchEvent !== "undefined" && e instanceof TouchEvent) return onTouchEvent(e); 21237 } 21238 startMove(e) { 21239 this._executeRelevantHandler(e, (e) => { 21240 this.mouseMoveStateManager.startMove(e); 21241 }, (e) => { 21242 this.oneFingerTouchMoveStateManager.startMove(e); 21243 }); 21244 } 21245 endMove(e) { 21246 this._executeRelevantHandler(e, (e) => { 21247 this.mouseMoveStateManager.endMove(e); 21248 }, (e) => { 21249 this.oneFingerTouchMoveStateManager.endMove(e); 21250 }); 21251 } 21252 isValidStartEvent(e) { 21253 return !!this._executeRelevantHandler(e, (e) => this.mouseMoveStateManager.isValidStartEvent(e), (e) => this.oneFingerTouchMoveStateManager.isValidStartEvent(e)); 21254 } 21255 isValidMoveEvent(e) { 21256 return !!this._executeRelevantHandler(e, (e) => this.mouseMoveStateManager.isValidMoveEvent(e), (e) => this.oneFingerTouchMoveStateManager.isValidMoveEvent(e)); 21257 } 21258 isValidEndEvent(e) { 21259 return !!this._executeRelevantHandler(e, (e) => this.mouseMoveStateManager.isValidEndEvent(e), (e) => this.oneFingerTouchMoveStateManager.isValidEndEvent(e)); 21260 } 21261}; 21262//#endregion 21263//#region src/ui/handler/mouse.ts 21264const LEFT_BUTTON = 0; 21265const RIGHT_BUTTON = 2; 21266const assignEvents = (handler) => { 21267 handler.mousedown = handler.dragStart; 21268 handler.mousemoveWindow = handler.dragMove; 21269 handler.mouseup = handler.dragEnd; 21270 handler.contextmenu = (e) => { 21271 e.preventDefault(); 21272 }; 21273}; 21274function generateMousePanHandler({ enable, clickTolerance }) { 21275 return new DragHandler({ 21276 clickTolerance, 21277 move: (lastPoint, point) => ({ 21278 around: point, 21279 panDelta: point.sub(lastPoint) 21280 }), 21281 activateOnStart: true, 21282 moveStateManager: new MouseMoveStateManager({ checkCorrectEvent: (e) => e.button === LEFT_BUTTON && !e.ctrlKey }), 21283 enable, 21284 assignEvents 21285 }); 21286} 21287function generateMouseRotationHandler({ enable, clickTolerance, aroundCenter = true, minPixelCenterThreshold = 100, rotateSpeed = .8 }, getCenter) { 21288 return new DragHandler({ 21289 clickTolerance, 21290 move: (lastPoint, currentPoint) => { 21291 const center = getCenter(); 21292 if (aroundCenter && Math.abs(center.y - lastPoint.y) > minPixelCenterThreshold) return { bearingDelta: getAngleDelta(new Point(lastPoint.x, currentPoint.y), currentPoint, center) }; 21293 let bearingDelta = (currentPoint.x - lastPoint.x) * rotateSpeed; 21294 if (aroundCenter && currentPoint.y < center.y) bearingDelta = -bearingDelta; 21295 return { bearingDelta }; 21296 }, 21297 moveStateManager: new MouseMoveStateManager({ checkCorrectEvent: (e) => e.button === LEFT_BUTTON && e.ctrlKey || e.button === RIGHT_BUTTON && !e.ctrlKey }), 21298 enable, 21299 assignEvents 21300 }); 21301} 21302function generateMousePitchHandler({ enable, clickTolerance, pitchSpeed = -.5 }) { 21303 return new DragHandler({ 21304 clickTolerance, 21305 move: (lastPoint, point) => ({ pitchDelta: (point.y - lastPoint.y) * pitchSpeed }), 21306 moveStateManager: new MouseMoveStateManager({ checkCorrectEvent: (e) => e.button === LEFT_BUTTON && e.ctrlKey || e.button === RIGHT_BUTTON }), 21307 enable, 21308 assignEvents 21309 }); 21310} 21311function generateMouseRollHandler({ enable, clickTolerance, rollDegreesPerPixelMoved = .3 }, getCenter) { 21312 return new DragHandler({ 21313 clickTolerance, 21314 move: (lastPoint, currentPoint) => { 21315 const center = getCenter(); 21316 let rollDelta = (currentPoint.x - lastPoint.x) * rollDegreesPerPixelMoved; 21317 if (currentPoint.y < center.y) rollDelta = -rollDelta; 21318 return { rollDelta }; 21319 }, 21320 moveStateManager: new MouseMoveStateManager({ checkCorrectEvent: (e) => e.button === RIGHT_BUTTON && e.ctrlKey }), 21321 enable, 21322 assignEvents 21323 }); 21324} 21325//#endregion 21326//#region src/ui/handler/touch_pan.ts 21327/** 21328* A `TouchPanHandler` allows the user to pan the map using touch gestures. 21329*/ 21330var TouchPanHandler = class { 21331 constructor(options, map) { 21332 this._clickTolerance = options.clickTolerance || 1; 21333 this._map = map; 21334 this.reset(); 21335 } 21336 reset() { 21337 this._active = false; 21338 this._touches = {}; 21339 this._sum = new Point(0, 0); 21340 } 21341 _shouldBePrevented(touchesCount) { 21342 return touchesCount < (this._map.cooperativeGestures.isEnabled() ? 2 : 1); 21343 } 21344 touchstart(e, points, mapTouches) { 21345 return this._calculateTransform(e, points, mapTouches); 21346 } 21347 touchmove(e, points, mapTouches) { 21348 if (!this._active) return; 21349 if (this._shouldBePrevented(mapTouches.length)) { 21350 this._map.cooperativeGestures.notifyGestureBlocked("touch_pan", e); 21351 return; 21352 } 21353 e.preventDefault(); 21354 return this._calculateTransform(e, points, mapTouches); 21355 } 21356 touchend(e, points, mapTouches) { 21357 this._calculateTransform(e, points, mapTouches); 21358 if (this._active && this._shouldBePrevented(mapTouches.length)) this.reset(); 21359 } 21360 touchcancel() { 21361 this.reset(); 21362 } 21363 _calculateTransform(e, points, mapTouches) { 21364 if (mapTouches.length > 0) this._active = true; 21365 const touches = indexTouches(mapTouches, points); 21366 const touchPointSum = new Point(0, 0); 21367 const touchDeltaSum = new Point(0, 0); 21368 let touchDeltaCount = 0; 21369 for (const identifier in touches) { 21370 const point = touches[identifier]; 21371 const prevPoint = this._touches[identifier]; 21372 if (prevPoint) { 21373 touchPointSum._add(point); 21374 touchDeltaSum._add(point.sub(prevPoint)); 21375 touchDeltaCount++; 21376 touches[identifier] = point; 21377 } 21378 } 21379 this._touches = touches; 21380 if (this._shouldBePrevented(touchDeltaCount) || !touchDeltaSum.mag()) return; 21381 const panDelta = touchDeltaSum.div(touchDeltaCount); 21382 this._sum._add(panDelta); 21383 if (this._sum.mag() < this._clickTolerance) return; 21384 return { 21385 around: touchPointSum.div(touchDeltaCount), 21386 panDelta 21387 }; 21388 } 21389 enable() { 21390 this._enabled = true; 21391 } 21392 disable() { 21393 this._enabled = false; 21394 this.reset(); 21395 } 21396 isEnabled() { 21397 return this._enabled; 21398 } 21399 isActive() { 21400 return this._active; 21401 } 21402}; 21403//#endregion 21404//#region src/ui/handler/two_fingers_touch.ts 21405/** 21406* The `TwoFingersTouchHandler`s allows the user to zoom, pitch and rotate the map using two fingers 21407* 21408*/ 21409var TwoFingersTouchHandler = class { 21410 /** @internal */ 21411 constructor() { 21412 this.reset(); 21413 } 21414 reset() { 21415 this._active = false; 21416 delete this._firstTwoTouches; 21417 } 21418 touchstart(e, points, mapTouches) { 21419 if (this._firstTwoTouches || mapTouches.length < 2) return; 21420 this._firstTwoTouches = [mapTouches[0].identifier, mapTouches[1].identifier]; 21421 this._start([points[0], points[1]]); 21422 } 21423 touchmove(e, points, mapTouches) { 21424 if (!this._firstTwoTouches) return; 21425 e.preventDefault(); 21426 const [idA, idB] = this._firstTwoTouches; 21427 const a = getTouchById(mapTouches, points, idA); 21428 const b = getTouchById(mapTouches, points, idB); 21429 if (!a || !b) return; 21430 const pinchAround = this._aroundCenter ? null : a.add(b).div(2); 21431 return this._move([a, b], pinchAround, e); 21432 } 21433 touchend(e, points, mapTouches) { 21434 if (!this._firstTwoTouches) return; 21435 const [idA, idB] = this._firstTwoTouches; 21436 const a = getTouchById(mapTouches, points, idA); 21437 const b = getTouchById(mapTouches, points, idB); 21438 if (a && b) return; 21439 if (this._active) DOM.suppressClick(); 21440 this.reset(); 21441 } 21442 touchcancel() { 21443 this.reset(); 21444 } 21445 /** 21446 * Enables the "drag to pitch" interaction. 21447 * 21448 * @example 21449 * ```ts 21450 * map.touchPitch.enable(); 21451 * ``` 21452 */ 21453 enable(options) { 21454 this._enabled = true; 21455 this._aroundCenter = !!options && options.around === "center"; 21456 } 21457 /** 21458 * Disables the "drag to pitch" interaction. 21459 * 21460 * @example 21461 * ```ts 21462 * map.touchPitch.disable(); 21463 * ``` 21464 */ 21465 disable() { 21466 this._enabled = false; 21467 this.reset(); 21468 } 21469 /** 21470 * Returns a Boolean indicating whether the "drag to pitch" interaction is enabled. 21471 * 21472 * @returns `true` if the "drag to pitch" interaction is enabled. 21473 */ 21474 isEnabled() { 21475 return !!this._enabled; 21476 } 21477 /** 21478 * Returns a Boolean indicating whether the "drag to pitch" interaction is active, i.e. currently being used. 21479 * 21480 * @returns `true` if the "drag to pitch" interaction is active. 21481 */ 21482 isActive() { 21483 return !!this._active; 21484 } 21485}; 21486function getTouchById(mapTouches, points, identifier) { 21487 for (let i = 0; i < mapTouches.length; i++) if (mapTouches[i].identifier === identifier) return points[i]; 21488} 21489const defaultZoomRate$2 = 1; 21490const defaultZoomThreshold = .1; 21491function getZoomDelta(distance, lastDistance) { 21492 return Math.log(distance / lastDistance) / Math.LN2; 21493} 21494/** 21495* The `TwoFingersTouchHandler`s allows the user to zoom the map two fingers 21496* 21497* @group Handlers 21498*/ 21499var TwoFingersTouchZoomHandler = class extends TwoFingersTouchHandler { 21500 constructor() { 21501 super(); 21502 this._zoomRate = defaultZoomRate$2; 21503 this._zoomThreshold = defaultZoomThreshold; 21504 } 21505 /** 21506 * Sets the zoom rate of touch gestures. 21507 * @param zoomRate - 1 The rate used to scale touch movement to a zoom value. Set to `undefined` to restore the default. 21508 * @example 21509 * Slow down touch zoom 21510 * ```ts 21511 * map.touchZoomRotate.setZoomRate(0.5); 21512 * ``` 21513 */ 21514 setZoomRate(zoomRate) { 21515 this._zoomRate = zoomRate ?? defaultZoomRate$2; 21516 } 21517 /** 21518 * Sets the threshold before a pinch gesture starts zooming. 21519 * @param zoomThreshold - 0.1 The minimum zoom delta before the pinch gesture becomes active. Set to `undefined` to restore the default. 21520 * @example 21521 * Make pinch zoom less sensitive 21522 * ```ts 21523 * map.touchZoomRotate.setZoomThreshold(0.3); 21524 * ``` 21525 */ 21526 setZoomThreshold(zoomThreshold) { 21527 this._zoomThreshold = zoomThreshold ?? defaultZoomThreshold; 21528 } 21529 reset() { 21530 super.reset(); 21531 delete this._distance; 21532 delete this._startDistance; 21533 } 21534 _start(points) { 21535 this._startDistance = this._distance = points[0].dist(points[1]); 21536 } 21537 _move(points, pinchAround) { 21538 const lastDistance = this._distance; 21539 this._distance = points[0].dist(points[1]); 21540 if (!this._active && Math.abs(getZoomDelta(this._distance, this._startDistance)) < this._zoomThreshold) return; 21541 this._active = true; 21542 return { 21543 zoomDelta: getZoomDelta(this._distance, lastDistance) * this._zoomRate, 21544 pinchAround 21545 }; 21546 } 21547}; 21548const ROTATION_THRESHOLD = 25; 21549function getBearingDelta(a, b) { 21550 return a.angleWith(b) * 180 / Math.PI; 21551} 21552/** 21553* The `TwoFingersTouchHandler`s allows the user to rotate the map two fingers 21554* 21555* @group Handlers 21556*/ 21557var TwoFingersTouchRotateHandler = class extends TwoFingersTouchHandler { 21558 reset() { 21559 super.reset(); 21560 delete this._minDiameter; 21561 delete this._startVector; 21562 delete this._vector; 21563 } 21564 _start(points) { 21565 this._startVector = this._vector = points[0].sub(points[1]); 21566 this._minDiameter = points[0].dist(points[1]); 21567 } 21568 _move(points, pinchAround, _e) { 21569 const lastVector = this._vector; 21570 this._vector = points[0].sub(points[1]); 21571 if (!this._active && this._isBelowThreshold(this._vector)) return; 21572 this._active = true; 21573 return { 21574 bearingDelta: getBearingDelta(this._vector, lastVector), 21575 pinchAround 21576 }; 21577 } 21578 _isBelowThreshold(vector) { 21579 this._minDiameter = Math.min(this._minDiameter, vector.mag()); 21580 const circumference = Math.PI * this._minDiameter; 21581 const threshold = ROTATION_THRESHOLD / circumference * 360; 21582 const bearingDeltaSinceStart = getBearingDelta(vector, this._startVector); 21583 return Math.abs(bearingDeltaSinceStart) < threshold; 21584 } 21585}; 21586function isVertical(vector) { 21587 return Math.abs(vector.y) > Math.abs(vector.x); 21588} 21589const ALLOWED_SINGLE_TOUCH_TIME = 100; 21590/** 21591* The `TwoFingersTouchPitchHandler` allows the user to pitch the map by dragging up and down with two fingers. 21592* 21593* @group Handlers 21594*/ 21595var TwoFingersTouchPitchHandler = class extends TwoFingersTouchHandler { 21596 constructor(map) { 21597 super(); 21598 this._currentTouchCount = 0; 21599 this._map = map; 21600 } 21601 reset() { 21602 super.reset(); 21603 this._valid = void 0; 21604 delete this._firstMove; 21605 delete this._lastPoints; 21606 } 21607 touchstart(e, points, mapTouches) { 21608 super.touchstart(e, points, mapTouches); 21609 this._currentTouchCount = mapTouches.length; 21610 } 21611 _start(points) { 21612 this._lastPoints = points; 21613 if (isVertical(points[0].sub(points[1]))) this._valid = false; 21614 } 21615 _move(points, center, e) { 21616 if (this._map.cooperativeGestures.isEnabled() && this._currentTouchCount < 3) return; 21617 const vectorA = points[0].sub(this._lastPoints[0]); 21618 const vectorB = points[1].sub(this._lastPoints[1]); 21619 this._valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp); 21620 if (!this._valid) return; 21621 this._lastPoints = points; 21622 this._active = true; 21623 return { pitchDelta: (vectorA.y + vectorB.y) / 2 * -.5 }; 21624 } 21625 gestureBeginsVertically(vectorA, vectorB, timeStamp) { 21626 if (this._valid !== void 0) return this._valid; 21627 const threshold = 2; 21628 const movedA = vectorA.mag() >= threshold; 21629 const movedB = vectorB.mag() >= threshold; 21630 if (!movedA && !movedB) return; 21631 if (!movedA || !movedB) { 21632 if (this._firstMove === void 0) this._firstMove = timeStamp; 21633 if (timeStamp - this._firstMove < ALLOWED_SINGLE_TOUCH_TIME) return; 21634 else return false; 21635 } 21636 const isSameDirection = vectorA.y > 0 === vectorB.y > 0; 21637 return isVertical(vectorA) && isVertical(vectorB) && isSameDirection; 21638 } 21639}; 21640//#endregion 21641//#region src/ui/handler/keyboard.ts 21642const defaultOptions$5 = { 21643 panStep: 100, 21644 bearingStep: 15, 21645 pitchStep: 10 21646}; 21647/** 21648* The `KeyboardHandler` allows the user to zoom, rotate, and pan the map using 21649* the following keyboard shortcuts: 21650* 21651* - `=` / `+`: Increase the zoom level by 1. 21652* - `Shift-=` / `Shift-+`: Increase the zoom level by 2. 21653* - `-`: Decrease the zoom level by 1. 21654* - `Shift--`: Decrease the zoom level by 2. 21655* - Arrow keys: Pan by 100 pixels. 21656* - `Shift+â¢`: Increase the rotation by 15 degrees. 21657* - `Shift+â `: Decrease the rotation by 15 degrees. 21658* - `Shift+â¡`: Increase the pitch by 10 degrees. 21659* - `Shift+â£`: Decrease the pitch by 10 degrees. 21660* 21661* @group Handlers 21662*/ 21663var KeyboardHandler = class { 21664 /** @internal */ 21665 constructor(map, transformProvider) { 21666 this._tr = transformProvider; 21667 const stepOptions = defaultOptions$5; 21668 this._panStep = stepOptions.panStep; 21669 this._bearingStep = stepOptions.bearingStep; 21670 this._pitchStep = stepOptions.pitchStep; 21671 this._rotationDisabled = false; 21672 } 21673 reset() { 21674 this._active = false; 21675 } 21676 keydown(e) { 21677 if (e.altKey || e.ctrlKey || e.metaKey) return; 21678 let zoomDir = 0; 21679 let bearingDir = 0; 21680 let pitchDir = 0; 21681 let xDir = 0; 21682 let yDir = 0; 21683 switch (e.keyCode) { 21684 case 61: 21685 case 107: 21686 case 171: 21687 case 187: 21688 zoomDir = 1; 21689 break; 21690 case 189: 21691 case 109: 21692 case 173: 21693 zoomDir = -1; 21694 break; 21695 case 37: 21696 if (e.shiftKey) bearingDir = -1; 21697 else { 21698 e.preventDefault(); 21699 xDir = -1; 21700 } 21701 break; 21702 case 39: 21703 if (e.shiftKey) bearingDir = 1; 21704 else { 21705 e.preventDefault(); 21706 xDir = 1; 21707 } 21708 break; 21709 case 38: 21710 if (e.shiftKey) pitchDir = 1; 21711 else { 21712 e.preventDefault(); 21713 yDir = -1; 21714 } 21715 break; 21716 case 40: 21717 if (e.shiftKey) pitchDir = -1; 21718 else { 21719 e.preventDefault(); 21720 yDir = 1; 21721 } 21722 break; 21723 default: return; 21724 } 21725 if (this._rotationDisabled) { 21726 bearingDir = 0; 21727 pitchDir = 0; 21728 } 21729 return { cameraAnimation: (map) => { 21730 const tr = this._tr; 21731 map.easeTo({ 21732 duration: 300, 21733 easeId: "keyboardHandler", 21734 easing: easeOut, 21735 zoom: zoomDir ? evaluateZoomSnap(tr.zoom + zoomDir * (e.shiftKey ? 2 : 1), map.getZoomSnap()) : tr.zoom, 21736 bearing: tr.bearing + bearingDir * this._bearingStep, 21737 pitch: tr.pitch + pitchDir * this._pitchStep, 21738 offset: [-xDir * this._panStep, -yDir * this._panStep], 21739 center: tr.center 21740 }, { originalEvent: e }); 21741 } }; 21742 } 21743 /** 21744 * Enables the "keyboard rotate and zoom" interaction. 21745 * 21746 * @example 21747 * ```ts 21748 * map.keyboard.enable(); 21749 * ``` 21750 */ 21751 enable() { 21752 this._enabled = true; 21753 } 21754 /** 21755 * Disables the "keyboard rotate and zoom" interaction. 21756 * 21757 * @example 21758 * ```ts 21759 * map.keyboard.disable(); 21760 * ``` 21761 */ 21762 disable() { 21763 this._enabled = false; 21764 this.reset(); 21765 } 21766 /** 21767 * Returns a Boolean indicating whether the "keyboard rotate and zoom" 21768 * interaction is enabled. 21769 * 21770 * @returns `true` if the "keyboard rotate and zoom" 21771 * interaction is enabled. 21772 */ 21773 isEnabled() { 21774 return this._enabled; 21775 } 21776 /** 21777 * Returns true if the handler is enabled and has detected the start of a 21778 * zoom/rotate gesture. 21779 * 21780 * @returns `true` if the handler is enabled and has detected the 21781 * start of a zoom/rotate gesture. 21782 */ 21783 isActive() { 21784 return this._active; 21785 } 21786 /** 21787 * Disables the "keyboard pan/rotate" interaction, leaving the 21788 * "keyboard zoom" interaction enabled. 21789 * 21790 * @example 21791 * ```ts 21792 * map.keyboard.disableRotation(); 21793 * ``` 21794 */ 21795 disableRotation() { 21796 this._rotationDisabled = true; 21797 } 21798 /** 21799 * Enables the "keyboard pan/rotate" interaction. 21800 * 21801 * @example 21802 * ```ts 21803 * map.keyboard.enable(); 21804 * map.keyboard.enableRotation(); 21805 * ``` 21806 */ 21807 enableRotation() { 21808 this._rotationDisabled = false; 21809 } 21810}; 21811function easeOut(t) { 21812 return t * (2 - t); 21813} 21814//#endregion 21815//#region src/ui/handler/scroll_zoom.ts 21816const wheelZoomDelta = 4.000244140625; 21817const defaultZoomRate$1 = 1 / 100; 21818const wheelZoomRate = 1 / 450; 21819const maxScalePerFrame = 2; 21820const wheelEventTimeDiffAdjustment = 5; 21821/** 21822* The `ScrollZoomHandler` allows the user to zoom the map by scrolling. 21823* 21824* @group Handlers 21825*/ 21826var ScrollZoomHandler = class { 21827 /** @internal */ 21828 constructor(map, triggerRenderFrame, transformProvider) { 21829 this._onTimeout = (initialEvent) => { 21830 this._type = "wheel"; 21831 this._delta -= this._lastValue; 21832 if (!this._active) this._start(initialEvent); 21833 }; 21834 this._map = map; 21835 this._tr = transformProvider; 21836 this._triggerRenderFrame = triggerRenderFrame; 21837 this._delta = 0; 21838 this._defaultZoomRate = defaultZoomRate$1; 21839 this._wheelZoomRate = wheelZoomRate; 21840 } 21841 /** 21842 * Set the zoom rate of a trackpad 21843 * @param zoomRate - 1/100 The rate used to scale trackpad movement to a zoom value. 21844 * @example 21845 * Speed up trackpad zoom 21846 * ```ts 21847 * map.scrollZoom.setZoomRate(1/25); 21848 * ``` 21849 */ 21850 setZoomRate(zoomRate) { 21851 this._defaultZoomRate = zoomRate; 21852 } 21853 /** 21854 * Set the zoom rate of a mouse wheel 21855 * @param wheelZoomRate - 1/450 The rate used to scale mouse wheel movement to a zoom value. 21856 * @example 21857 * Slow down zoom of mouse wheel 21858 * ```ts 21859 * map.scrollZoom.setWheelZoomRate(1/600); 21860 * ``` 21861 */ 21862 setWheelZoomRate(wheelZoomRate) { 21863 this._wheelZoomRate = wheelZoomRate; 21864 } 21865 /** 21866 * Returns a Boolean indicating whether the "scroll to zoom" interaction is enabled. 21867 * @returns `true` if the "scroll to zoom" interaction is enabled. 21868 */ 21869 isEnabled() { 21870 return !!this._enabled; 21871 } 21872 isActive() { 21873 return !!this._active || this._finishTimeout !== void 0; 21874 } 21875 isZooming() { 21876 return !!this._zooming; 21877 } 21878 /** 21879 * Enables the "scroll to zoom" interaction. 21880 * 21881 * @param options - Options object. 21882 * @example 21883 * ```ts 21884 * map.scrollZoom.enable(); 21885 * map.scrollZoom.enable({ around: 'center' }) 21886 * ``` 21887 */ 21888 enable(options) { 21889 if (this.isEnabled()) return; 21890 this._enabled = true; 21891 this._aroundCenter = !!options && options.around === "center"; 21892 } 21893 /** 21894 * Disables the "scroll to zoom" interaction. 21895 * 21896 * @example 21897 * ```ts 21898 * map.scrollZoom.disable(); 21899 * ``` 21900 */ 21901 disable() { 21902 if (!this.isEnabled()) return; 21903 this._enabled = false; 21904 } 21905 /** 21906 * Determines whether or not the gesture is blocked due to cooperativeGestures. 21907 */ 21908 _shouldBePrevented(e) { 21909 if (!this._map.cooperativeGestures.isEnabled()) return false; 21910 return !(e.ctrlKey || this._map.cooperativeGestures.isBypassed(e)); 21911 } 21912 wheel(e) { 21913 if (!this.isEnabled()) return; 21914 if (this._shouldBePrevented(e)) { 21915 this._map.cooperativeGestures.notifyGestureBlocked("wheel_zoom", e); 21916 return; 21917 } 21918 let value = e.deltaMode === WheelEvent.DOM_DELTA_LINE ? e.deltaY * 40 : e.deltaY; 21919 const currentTime = now(), timeDelta = currentTime - (this._lastWheelEventTime || 0); 21920 this._lastWheelEventTime = currentTime; 21921 if (value !== 0 && value % wheelZoomDelta === 0) this._type = "wheel"; 21922 else if (value !== 0 && Math.abs(value) < 4) this._type = "trackpad"; 21923 else if (timeDelta > 400) { 21924 this._type = null; 21925 this._lastValue = value; 21926 this._timeout = setTimeout(this._onTimeout, 40, e); 21927 } else if (!this._type) { 21928 this._type = Math.abs(timeDelta * value) < 200 ? "trackpad" : "wheel"; 21929 if (this._timeout) { 21930 clearTimeout(this._timeout); 21931 this._timeout = null; 21932 value += this._lastValue; 21933 } 21934 } 21935 if (e.shiftKey && value) value = value / 4; 21936 if (this._type) { 21937 this._lastWheelEvent = e; 21938 this._delta -= value; 21939 if (!this._active) this._start(e); 21940 } 21941 e.preventDefault(); 21942 } 21943 _start(e) { 21944 if (!this._delta) return; 21945 this._needsRerender = false; 21946 this._active = true; 21947 if (!this.isZooming()) this._zooming = true; 21948 if (this._finishTimeout) { 21949 clearTimeout(this._finishTimeout); 21950 delete this._finishTimeout; 21951 } 21952 const pos = DOM.mousePos(this._map.getCanvas(), e); 21953 const tr = this._tr; 21954 if (this._aroundCenter) this._aroundPoint = tr.transform.locationToScreenPoint(LngLat.convert(tr.center)); 21955 else this._aroundPoint = pos; 21956 if (!this._needsRerender) { 21957 this._needsRerender = true; 21958 this._triggerRenderFrame(); 21959 } 21960 } 21961 renderFrame() { 21962 if (!this._needsRerender) return; 21963 this._needsRerender = false; 21964 if (!this.isActive()) return; 21965 const tr = this._tr.transform; 21966 if (typeof this._lastExpectedZoom === "number") { 21967 const externalZoomChange = tr.zoom - this._lastExpectedZoom; 21968 if (typeof this._startZoom === "number") this._startZoom += externalZoomChange; 21969 if (typeof this._targetZoom === "number") this._targetZoom += externalZoomChange; 21970 } 21971 if (this._delta !== 0) { 21972 const zoomRate = this._type === "wheel" && Math.abs(this._delta) > wheelZoomDelta ? this._wheelZoomRate : this._defaultZoomRate; 21973 let scale = maxScalePerFrame / (1 + Math.exp(-Math.abs(this._delta * zoomRate))); 21974 if (this._delta < 0 && scale !== 0) scale = 1 / scale; 21975 const fromScale = typeof this._targetZoom !== "number" ? tr.scale : zoomScale(this._targetZoom); 21976 const target = tr.applyConstrain(tr.getCameraLngLat(), scaleZoom(fromScale * scale)).zoom; 21977 const zoomSnap = this._map.getZoomSnap(); 21978 if (this._type === "wheel" && zoomSnap > 0) { 21979 const currentSnapped = evaluateZoomSnap(tr.zoom, zoomSnap); 21980 this._targetZoom = evaluateZoomSnap(target, zoomSnap, target - currentSnapped); 21981 } else this._targetZoom = target; 21982 if (this._type === "wheel") { 21983 this._startZoom = tr.zoom; 21984 this._easing = this._smoothOutEasing(200); 21985 } 21986 this._delta = 0; 21987 } 21988 const targetZoom = typeof this._targetZoom !== "number" ? tr.zoom : this._targetZoom; 21989 const startZoom = this._startZoom; 21990 const easing = this._easing; 21991 let finished = false; 21992 let zoom; 21993 if (this._type === "wheel" && startZoom && easing) { 21994 const lastWheelEventTimeDiff = now() - this._lastWheelEventTime; 21995 const t = Math.min((lastWheelEventTimeDiff + wheelEventTimeDiffAdjustment) / 200, 1); 21996 const k = easing(t); 21997 zoom = interpolateFactory.number(startZoom, targetZoom, k); 21998 if (t < 1) this._needsRerender = true; 21999 else finished = true; 22000 } else { 22001 zoom = targetZoom; 22002 finished = true; 22003 } 22004 this._active = true; 22005 if (finished) { 22006 this._active = false; 22007 this._finishTimeout = setTimeout(() => { 22008 this._zooming = false; 22009 this._triggerRenderFrame(); 22010 delete this._targetZoom; 22011 delete this._lastExpectedZoom; 22012 delete this._finishTimeout; 22013 }, 200); 22014 } 22015 this._lastExpectedZoom = zoom; 22016 return { 22017 noInertia: true, 22018 needsRenderFrame: !finished, 22019 zoomDelta: zoom - tr.zoom, 22020 around: this._aroundPoint, 22021 originalEvent: this._lastWheelEvent 22022 }; 22023 } 22024 _smoothOutEasing(duration) { 22025 let easing = defaultEasing; 22026 if (this._prevEase) { 22027 const currentEase = this._prevEase; 22028 const t = (now() - currentEase.start) / currentEase.duration; 22029 const speed = currentEase.easing(t + .01) - currentEase.easing(t); 22030 const x = .27 / Math.sqrt(speed * speed + 1e-4) * .01; 22031 const y = Math.sqrt(.0729 - x * x); 22032 easing = bezier(x, y, .25, 1); 22033 } 22034 this._prevEase = { 22035 start: now(), 22036 duration, 22037 easing 22038 }; 22039 return easing; 22040 } 22041 reset() { 22042 this._active = false; 22043 this._zooming = false; 22044 delete this._targetZoom; 22045 delete this._lastExpectedZoom; 22046 if (this._finishTimeout) { 22047 clearTimeout(this._finishTimeout); 22048 delete this._finishTimeout; 22049 } 22050 } 22051}; 22052//#endregion 22053//#region src/ui/handler/shim/dblclick_zoom.ts 22054/** 22055* The `DoubleClickZoomHandler` allows the user to zoom the map at a point by 22056* double clicking or double tapping. 22057* 22058* @group Handlers 22059*/ 22060var DoubleClickZoomHandler = class { 22061 /** @internal */ 22062 constructor(clickZoom, TapZoom) { 22063 this._clickZoom = clickZoom; 22064 this._tapZoom = TapZoom; 22065 } 22066 /** 22067 * Enables the "double click to zoom" interaction. 22068 * 22069 * @example 22070 * ```ts 22071 * map.doubleClickZoom.enable(); 22072 * ``` 22073 */ 22074 enable() { 22075 this._clickZoom.enable(); 22076 this._tapZoom.enable(); 22077 } 22078 /** 22079 * Disables the "double click to zoom" interaction. 22080 * 22081 * @example 22082 * ```ts 22083 * map.doubleClickZoom.disable(); 22084 * ``` 22085 */ 22086 disable() { 22087 this._clickZoom.disable(); 22088 this._tapZoom.disable(); 22089 } 22090 /** 22091 * Returns a Boolean indicating whether the "double click to zoom" interaction is enabled. 22092 * 22093 * @returns `true` if the "double click to zoom" interaction is enabled. 22094 */ 22095 isEnabled() { 22096 return this._clickZoom.isEnabled() && this._tapZoom.isEnabled(); 22097 } 22098 /** 22099 * Returns a Boolean indicating whether the "double click to zoom" interaction is active, i.e. currently being used. 22100 * 22101 * @returns `true` if the "double click to zoom" interaction is active. 22102 */ 22103 isActive() { 22104 return this._clickZoom.isActive() || this._tapZoom.isActive(); 22105 } 22106}; 22107//#endregion 22108//#region src/ui/handler/click_zoom.ts 22109/** 22110* The `ClickZoomHandler` allows the user to zoom the map at a point by double clicking 22111* It is used by other handlers 22112*/ 22113var ClickZoomHandler = class { 22114 /** @internal */ 22115 constructor(map, transformProvider) { 22116 this._tr = transformProvider; 22117 this.reset(); 22118 } 22119 reset() { 22120 this._active = false; 22121 } 22122 dblclick(e, point) { 22123 e.preventDefault(); 22124 return { cameraAnimation: (map) => { 22125 map.easeTo({ 22126 duration: 300, 22127 zoom: evaluateZoomSnap(this._tr.zoom + (e.shiftKey ? -1 : 1), map.getZoomSnap()), 22128 around: this._tr.unproject(point) 22129 }, { originalEvent: e }); 22130 } }; 22131 } 22132 enable() { 22133 this._enabled = true; 22134 } 22135 disable() { 22136 this._enabled = false; 22137 this.reset(); 22138 } 22139 isEnabled() { 22140 return this._enabled; 22141 } 22142 isActive() { 22143 return this._active; 22144 } 22145}; 22146//#endregion 22147//#region src/ui/handler/tap_drag_zoom.ts 22148const defaultZoomRate = 1; 22149/** 22150* A `TapDragZoomHandler` allows the user to zoom the map at a point by double tapping. It also allows the user pan the map by dragging. 22151*/ 22152var TapDragZoomHandler = class { 22153 constructor() { 22154 this._tap = new TapRecognizer({ 22155 numTouches: 1, 22156 numTaps: 1 22157 }); 22158 this._zoomRate = defaultZoomRate; 22159 this.reset(); 22160 } 22161 setZoomRate(zoomRate) { 22162 this._zoomRate = zoomRate ?? defaultZoomRate; 22163 } 22164 reset() { 22165 this._active = false; 22166 delete this._swipePoint; 22167 delete this._swipeTouch; 22168 delete this._tapTime; 22169 delete this._tapPoint; 22170 this._tap.reset(); 22171 } 22172 touchstart(e, points, mapTouches) { 22173 if (this._swipePoint) return; 22174 if (!this._tapTime) this._tap.touchstart(e, points, mapTouches); 22175 else { 22176 const swipePoint = points[0]; 22177 const soonEnough = e.timeStamp - this._tapTime < 500; 22178 const closeEnough = this._tapPoint.dist(swipePoint) < 30; 22179 if (!soonEnough || !closeEnough) this.reset(); 22180 else if (mapTouches.length > 0) { 22181 this._swipePoint = swipePoint; 22182 this._swipeTouch = mapTouches[0].identifier; 22183 } 22184 } 22185 } 22186 touchmove(e, points, mapTouches) { 22187 if (!this._tapTime) this._tap.touchmove(e, points, mapTouches); 22188 else if (this._swipePoint) { 22189 if (mapTouches[0].identifier !== this._swipeTouch) return; 22190 const newSwipePoint = points[0]; 22191 const dist = newSwipePoint.y - this._swipePoint.y; 22192 this._swipePoint = newSwipePoint; 22193 e.preventDefault(); 22194 this._active = true; 22195 return { zoomDelta: dist / 128 * this._zoomRate }; 22196 } 22197 } 22198 touchend(e, points, mapTouches) { 22199 if (!this._tapTime) { 22200 const point = this._tap.touchend(e, points, mapTouches); 22201 if (point) { 22202 this._tapTime = e.timeStamp; 22203 this._tapPoint = point; 22204 } 22205 } else if (this._swipePoint) { 22206 if (mapTouches.length === 0) this.reset(); 22207 } 22208 } 22209 touchcancel() { 22210 this.reset(); 22211 } 22212 enable() { 22213 this._enabled = true; 22214 } 22215 disable() { 22216 this._enabled = false; 22217 this.reset(); 22218 } 22219 isEnabled() { 22220 return this._enabled; 22221 } 22222 isActive() { 22223 return this._active; 22224 } 22225}; 22226//#endregion 22227//#region src/ui/handler/shim/drag_pan.ts 22228/** 22229* The `DragPanHandler` allows the user to pan the map by clicking and dragging 22230* the cursor. 22231* 22232* @group Handlers 22233*/ 22234var DragPanHandler = class { 22235 /** @internal */ 22236 constructor(el, mousePan, touchPan) { 22237 this._el = el; 22238 this._mousePan = mousePan; 22239 this._touchPan = touchPan; 22240 } 22241 /** 22242 * Enables the "drag to pan" interaction. 22243 * 22244 * @param options - Options object 22245 * @example 22246 * ```ts 22247 * map.dragPan.enable(); 22248 * map.dragPan.enable({ 22249 * linearity: 0.3, 22250 * easing: bezier(0, 0, 0.3, 1), 22251 * maxSpeed: 1400, 22252 * deceleration: 2500, 22253 * }); 22254 * ``` 22255 */ 22256 enable(options) { 22257 this._inertiaOptions = options || {}; 22258 this._mousePan.enable(); 22259 this._touchPan.enable(); 22260 this._el.classList.add("maplibregl-touch-drag-pan"); 22261 } 22262 /** 22263 * Disables the "drag to pan" interaction. 22264 * 22265 * @example 22266 * ```ts 22267 * map.dragPan.disable(); 22268 * ``` 22269 */ 22270 disable() { 22271 this._mousePan.disable(); 22272 this._touchPan.disable(); 22273 this._el.classList.remove("maplibregl-touch-drag-pan"); 22274 } 22275 /** 22276 * Returns a Boolean indicating whether the "drag to pan" interaction is enabled. 22277 * 22278 * @returns `true` if the "drag to pan" interaction is enabled. 22279 */ 22280 isEnabled() { 22281 return this._mousePan.isEnabled() && this._touchPan.isEnabled(); 22282 } 22283 /** 22284 * Returns a Boolean indicating whether the "drag to pan" interaction is active, i.e. currently being used. 22285 * 22286 * @returns `true` if the "drag to pan" interaction is active. 22287 */ 22288 isActive() { 22289 return this._mousePan.isActive() || this._touchPan.isActive(); 22290 } 22291}; 22292//#endregion 22293//#region src/ui/handler/shim/drag_rotate.ts 22294/** 22295* The `DragRotateHandler` allows the user to rotate the map by clicking and 22296* dragging the cursor while holding the right mouse button or `ctrl` key. 22297* 22298* @group Handlers 22299*/ 22300var DragRotateHandler = class { 22301 /** @internal */ 22302 constructor(options, mouseRotate, mousePitch, mouseRoll) { 22303 this._pitchWithRotate = options.pitchWithRotate; 22304 this._rollEnabled = options.rollEnabled; 22305 this._mouseRotate = mouseRotate; 22306 this._mousePitch = mousePitch; 22307 this._mouseRoll = mouseRoll; 22308 } 22309 /** 22310 * Enables the "drag to rotate" interaction. 22311 * 22312 * @example 22313 * ```ts 22314 * map.dragRotate.enable(); 22315 * ``` 22316 */ 22317 enable() { 22318 this._mouseRotate.enable(); 22319 if (this._pitchWithRotate) this._mousePitch.enable(); 22320 if (this._rollEnabled) this._mouseRoll.enable(); 22321 } 22322 /** 22323 * Disables the "drag to rotate" interaction. 22324 * 22325 * @example 22326 * ```ts 22327 * map.dragRotate.disable(); 22328 * ``` 22329 */ 22330 disable() { 22331 this._mouseRotate.disable(); 22332 this._mousePitch.disable(); 22333 this._mouseRoll.disable(); 22334 } 22335 /** 22336 * Returns a Boolean indicating whether the "drag to rotate" interaction is enabled. 22337 * 22338 * @returns `true` if the "drag to rotate" interaction is enabled. 22339 */ 22340 isEnabled() { 22341 return this._mouseRotate.isEnabled() && (!this._pitchWithRotate || this._mousePitch.isEnabled()) && (!this._rollEnabled || this._mouseRoll.isEnabled()); 22342 } 22343 /** 22344 * Returns a Boolean indicating whether the "drag to rotate" interaction is active, i.e. currently being used. 22345 * 22346 * @returns `true` if the "drag to rotate" interaction is active. 22347 */ 22348 isActive() { 22349 return this._mouseRotate.isActive() || this._mousePitch.isActive() || this._mouseRoll.isActive(); 22350 } 22351}; 22352//#endregion 22353//#region src/ui/handler/shim/two_fingers_touch.ts 22354/** 22355* The `TwoFingersTouchZoomRotateHandler` allows the user to zoom and rotate the map by 22356* pinching on a touchscreen. 22357* 22358* They can zoom with one finger by double tapping and dragging. On the second tap, 22359* hold the finger down and drag up or down to zoom in or out. 22360* 22361* @group Handlers 22362*/ 22363var TwoFingersTouchZoomRotateHandler = class { 22364 /** @internal */ 22365 constructor(el, touchZoom, touchRotate, tapDragZoom) { 22366 this._el = el; 22367 this._touchZoom = touchZoom; 22368 this._touchRotate = touchRotate; 22369 this._tapDragZoom = tapDragZoom; 22370 this._rotationDisabled = false; 22371 this._enabled = true; 22372 } 22373 /** 22374 * Enables the "pinch to rotate and zoom" interaction. 22375 * 22376 * @param options - Options object. 22377 * 22378 * @example 22379 * ```ts 22380 * map.touchZoomRotate.enable(); 22381 * map.touchZoomRotate.enable({ around: 'center' }); 22382 * ``` 22383 */ 22384 enable(options) { 22385 this._touchZoom.enable(options); 22386 if (!this._rotationDisabled) this._touchRotate.enable(options); 22387 this._tapDragZoom.enable(); 22388 this._el.classList.add("maplibregl-touch-zoom-rotate"); 22389 } 22390 /** 22391 * Disables the "pinch to rotate and zoom" interaction. 22392 * 22393 * @example 22394 * ```ts 22395 * map.touchZoomRotate.disable(); 22396 * ``` 22397 */ 22398 disable() { 22399 this._touchZoom.disable(); 22400 this._touchRotate.disable(); 22401 this._tapDragZoom.disable(); 22402 this._el.classList.remove("maplibregl-touch-zoom-rotate"); 22403 } 22404 /** 22405 * Returns a Boolean indicating whether the "pinch to rotate and zoom" interaction is enabled. 22406 * 22407 * @returns `true` if the "pinch to rotate and zoom" interaction is enabled. 22408 */ 22409 isEnabled() { 22410 return this._touchZoom.isEnabled() && (this._rotationDisabled || this._touchRotate.isEnabled()) && this._tapDragZoom.isEnabled(); 22411 } 22412 /** 22413 * Returns true if the handler is enabled and has detected the start of a zoom/rotate gesture. 22414 * 22415 * @returns `true` if the handler is active, `false` otherwise 22416 */ 22417 isActive() { 22418 return this._touchZoom.isActive() || this._touchRotate.isActive() || this._tapDragZoom.isActive(); 22419 } 22420 /** 22421 * Sets the zoom rate of touch gestures. 22422 * @param zoomRate - 1 The rate used to scale touch movement to a zoom value. Set to `undefined` to restore the default. 22423 * @example 22424 * Slow down touch zoom 22425 * ```ts 22426 * map.touchZoomRotate.setZoomRate(0.5); 22427 * ``` 22428 */ 22429 setZoomRate(zoomRate) { 22430 this._touchZoom.setZoomRate(zoomRate); 22431 this._tapDragZoom.setZoomRate(zoomRate); 22432 } 22433 /** 22434 * Sets the threshold before a pinch gesture starts zooming. 22435 * @param zoomThreshold - 0.1 The minimum zoom delta before the pinch gesture becomes active. Set to `undefined` to restore the default. 22436 * @example 22437 * Make pinch zoom less sensitive 22438 * ```ts 22439 * map.touchZoomRotate.setZoomThreshold(0.3); 22440 * ``` 22441 */ 22442 setZoomThreshold(zoomThreshold) { 22443 this._touchZoom.setZoomThreshold(zoomThreshold); 22444 } 22445 /** 22446 * Disables the "pinch to rotate" interaction, leaving the "pinch to zoom" 22447 * interaction enabled. 22448 * 22449 * @example 22450 * ```ts 22451 * map.touchZoomRotate.disableRotation(); 22452 * ``` 22453 */ 22454 disableRotation() { 22455 this._rotationDisabled = true; 22456 this._touchRotate.disable(); 22457 } 22458 /** 22459 * Enables the "pinch to rotate" interaction. 22460 * 22461 * @example 22462 * ```ts 22463 * map.touchZoomRotate.enable(); 22464 * map.touchZoomRotate.enableRotation(); 22465 * ``` 22466 */ 22467 enableRotation() { 22468 this._rotationDisabled = false; 22469 if (this._touchZoom.isEnabled()) this._touchRotate.enable(); 22470 } 22471}; 22472//#endregion 22473//#region src/ui/handler/cooperative_gestures.ts 22474/** 22475* A `CooperativeGestureHandler` is a control that adds cooperative gesture info when user tries to zoom in/out. 22476* 22477* When the CooperativeGestureHandler blocks a gesture, it will emit a `cooperativegestureprevented` event. 22478* 22479* @group Handlers 22480* 22481* @example 22482* ```ts 22483* const map = new Map({ 22484* cooperativeGestures: true 22485* }); 22486* ``` 22487* @see [Example: cooperative gestures](https://maplibre.org/maplibre-gl-js/docs/examples/cooperative-gestures/) 22488**/ 22489var CooperativeGesturesHandler = class { 22490 constructor(map, options) { 22491 this._bypassKey = navigator.userAgent.includes("Mac") ? "metaKey" : "ctrlKey"; 22492 this._map = map; 22493 this._options = options; 22494 this._enabled = false; 22495 } 22496 isActive() { 22497 return false; 22498 } 22499 reset() {} 22500 _setupUI() { 22501 if (this._container) return; 22502 const mapCanvasContainer = this._map.getCanvasContainer(); 22503 mapCanvasContainer.classList.add("maplibregl-cooperative-gestures"); 22504 this._container = DOM.create("div", "maplibregl-cooperative-gesture-screen", mapCanvasContainer); 22505 let desktopMessage = this._map._getUIString("CooperativeGesturesHandler.WindowsHelpText"); 22506 if (this._bypassKey === "metaKey") desktopMessage = this._map._getUIString("CooperativeGesturesHandler.MacHelpText"); 22507 const mobileMessage = this._map._getUIString("CooperativeGesturesHandler.MobileHelpText"); 22508 const desktopDiv = document.createElement("div"); 22509 desktopDiv.className = "maplibregl-desktop-message"; 22510 desktopDiv.textContent = desktopMessage; 22511 this._container.appendChild(desktopDiv); 22512 const mobileDiv = document.createElement("div"); 22513 mobileDiv.className = "maplibregl-mobile-message"; 22514 mobileDiv.textContent = mobileMessage; 22515 this._container.appendChild(mobileDiv); 22516 this._container.setAttribute("aria-hidden", "true"); 22517 } 22518 _destroyUI() { 22519 if (this._container) { 22520 this._container.remove(); 22521 this._map.getCanvasContainer().classList.remove("maplibregl-cooperative-gestures"); 22522 } 22523 delete this._container; 22524 } 22525 enable() { 22526 this._setupUI(); 22527 this._enabled = true; 22528 } 22529 disable() { 22530 this._enabled = false; 22531 this._destroyUI(); 22532 } 22533 isEnabled() { 22534 return this._enabled; 22535 } 22536 isBypassed(event) { 22537 return event[this._bypassKey]; 22538 } 22539 notifyGestureBlocked(gestureType, originalEvent) { 22540 if (!this._enabled) return; 22541 this._map.fire(new MapLibreEvent("cooperativegestureprevented", { 22542 gestureType, 22543 originalEvent 22544 })); 22545 this._container.classList.add("maplibregl-show"); 22546 setTimeout(() => { 22547 this._container.classList.remove("maplibregl-show"); 22548 }, 100); 22549 } 22550}; 22551//#endregion 22552//#region src/ui/handler/transform-provider.ts 22553/** 22554* @internal 22555* Shared utilities for the Handler classes to access the correct camera state. 22556* If Camera.transformCameraUpdate is specified or terrain is enabled, the 22557* "desired state" of camera may differ from the state used for rendering. The 22558* handlers need the "desired state" to track accumulated changes. 22559*/ 22560var TransformProvider = class { 22561 constructor(camera) { 22562 this._camera = camera; 22563 } 22564 get transform() { 22565 return this._camera._requestedCameraState || this._camera.transform; 22566 } 22567 get center() { 22568 return { 22569 lng: this.transform.center.lng, 22570 lat: this.transform.center.lat 22571 }; 22572 } 22573 get zoom() { 22574 return this.transform.zoom; 22575 } 22576 get pitch() { 22577 return this.transform.pitch; 22578 } 22579 get bearing() { 22580 return this.transform.bearing; 22581 } 22582 unproject(point) { 22583 return this.transform.screenPointToLocation(Point.convert(point), this._camera.terrain); 22584 } 22585}; 22586//#endregion 22587//#region src/ui/handler_manager.ts 22588const isMoving = (p) => p.zoom || p.drag || p.roll || p.pitch || p.rotate; 22589/** 22590* A terrain gesture's anchor plane must stay below this fraction of the camera's 22591* altitude over the center-elevation plane, or the ray-plane solve degenerates. 22592*/ 22593const TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION = .9; 22594var RenderFrameEvent = class extends Event {}; 22595function hasChange(result) { 22596 return result.panDelta?.mag() || result.zoomDelta || result.bearingDelta || result.pitchDelta || result.rollDelta; 22597} 22598var HandlerManager = class { 22599 /** 22600 * @internal 22601 * The document that contains the map container element, for cross-window support. 22602 */ 22603 get _ownerDocument() { 22604 return this._el?.ownerDocument || document; 22605 } 22606 /** 22607 * @internal 22608 * The window that contains the map container element, for cross-window support. 22609 */ 22610 get _ownerWindow() { 22611 return this._el?.ownerDocument?.defaultView || window; 22612 } 22613 constructor(map, camera, options) { 22614 this._terrainGesture = { 22615 inFlight: false, 22616 anchorElevation: null 22617 }; 22618 this.handleWindowEvent = (e) => { 22619 this.handleEvent(e, `${e.type}Window`); 22620 }; 22621 this.handleEvent = (e, eventName) => { 22622 if (e.type === "blur") { 22623 this.stop(true); 22624 return; 22625 } 22626 this._updatingCamera = true; 22627 const inputEvent = e.type === "renderFrame" ? void 0 : e; 22628 const mergedHandlerResult = { needsRenderFrame: false }; 22629 const eventsInProgress = {}; 22630 const activeHandlers = {}; 22631 for (const { handlerName, handler, allowed } of this._handlers) { 22632 if (!handler.isEnabled()) continue; 22633 let data; 22634 if (this._blockedByActive(activeHandlers, allowed, handlerName)) handler.reset(); 22635 else if (handler[eventName || e.type]) { 22636 if (isPointableEvent(e, eventName || e.type)) { 22637 const point = DOM.mousePos(this._map.getCanvas(), e); 22638 data = handler[eventName || e.type](e, point); 22639 } else if (isTouchableEvent(e, eventName || e.type)) { 22640 const eventTouches = e.touches; 22641 const mapTouches = this._getMapTouches(eventTouches); 22642 const points = DOM.touchPos(this._map.getCanvas(), mapTouches); 22643 data = handler[eventName || e.type](e, points, mapTouches); 22644 } else if (!isTouchableOrPointableType(eventName || e.type)) data = handler[eventName || e.type](e); 22645 this.mergeHandlerResult(mergedHandlerResult, eventsInProgress, data, handlerName, inputEvent); 22646 if (data?.needsRenderFrame) this._triggerRenderFrame(); 22647 } 22648 if (data || handler.isActive()) activeHandlers[handlerName] = handler; 22649 } 22650 const deactivatedHandlers = {}; 22651 for (const name in this._previousActiveHandlers) if (!activeHandlers[name]) deactivatedHandlers[name] = inputEvent; 22652 this._previousActiveHandlers = activeHandlers; 22653 if (Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult)) { 22654 this._changes.push([ 22655 mergedHandlerResult, 22656 eventsInProgress, 22657 deactivatedHandlers 22658 ]); 22659 this._triggerRenderFrame(); 22660 } 22661 if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) this._camera.stop(true); 22662 this._updatingCamera = false; 22663 const { cameraAnimation } = mergedHandlerResult; 22664 if (cameraAnimation) { 22665 this._inertia.clear(); 22666 this._fireEvents({}, {}, true); 22667 this._changes = []; 22668 cameraAnimation(this._map); 22669 } 22670 }; 22671 this._map = map; 22672 this._camera = camera; 22673 this._transformProvider = new TransformProvider(this._camera); 22674 this._el = this._map.getCanvasContainer(); 22675 this._handlers = []; 22676 this._handlersById = {}; 22677 this._changes = []; 22678 this._inertia = new HandlerInertia(map); 22679 this._bearingSnap = options.bearingSnap; 22680 this._previousActiveHandlers = {}; 22681 this._eventsInProgress = {}; 22682 this._addDefaultHandlers(options); 22683 const el = this._el; 22684 this._listeners = [ 22685 [ 22686 el, 22687 "touchstart", 22688 { passive: true } 22689 ], 22690 [ 22691 el, 22692 "touchmove", 22693 { passive: false } 22694 ], 22695 [ 22696 el, 22697 "touchend", 22698 void 0 22699 ], 22700 [ 22701 el, 22702 "touchcancel", 22703 void 0 22704 ], 22705 [ 22706 el, 22707 "mousedown", 22708 void 0 22709 ], 22710 [ 22711 el, 22712 "mousemove", 22713 void 0 22714 ], 22715 [ 22716 el, 22717 "mouseup", 22718 void 0 22719 ], 22720 [ 22721 this._ownerDocument, 22722 "mousemove", 22723 { capture: true } 22724 ], 22725 [ 22726 this._ownerDocument, 22727 "mouseup", 22728 void 0 22729 ], 22730 [ 22731 el, 22732 "mouseover", 22733 void 0 22734 ], 22735 [ 22736 el, 22737 "mouseout", 22738 void 0 22739 ], 22740 [ 22741 el, 22742 "dblclick", 22743 void 0 22744 ], 22745 [ 22746 el, 22747 "click", 22748 void 0 22749 ], 22750 [ 22751 el, 22752 "keydown", 22753 { capture: false } 22754 ], 22755 [ 22756 el, 22757 "keyup", 22758 void 0 22759 ], 22760 [ 22761 el, 22762 "wheel", 22763 { passive: false } 22764 ], 22765 [ 22766 el, 22767 "contextmenu", 22768 void 0 22769 ], 22770 [ 22771 this._ownerWindow, 22772 "blur", 22773 void 0 22774 ] 22775 ]; 22776 for (const [target, type, listenerOptions] of this._listeners) target.addEventListener(type, target === this._ownerDocument ? this.handleWindowEvent : this.handleEvent, listenerOptions); 22777 } 22778 destroy() { 22779 for (const [target, type, listenerOptions] of this._listeners) target.removeEventListener(type, target === this._ownerDocument ? this.handleWindowEvent : this.handleEvent, listenerOptions); 22780 } 22781 _addDefaultHandlers(options) { 22782 const map = this._map; 22783 const el = map.getCanvasContainer(); 22784 this._add("mapEvent", new MapEventHandler(map, options)); 22785 const boxZoom = map.boxZoom = new BoxZoomHandler(map, options, this._transformProvider); 22786 this._add("boxZoom", boxZoom); 22787 if (options.interactive && options.boxZoom) boxZoom.enable(); 22788 const cooperativeGestures = map.cooperativeGestures = new CooperativeGesturesHandler(map, options.cooperativeGestures); 22789 this._add("cooperativeGestures", cooperativeGestures); 22790 if (options.cooperativeGestures) cooperativeGestures.enable(); 22791 const tapZoom = new TapZoomHandler(map, this._transformProvider); 22792 const clickZoom = new ClickZoomHandler(map, this._transformProvider); 22793 map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); 22794 this._add("tapZoom", tapZoom); 22795 this._add("clickZoom", clickZoom); 22796 if (options.interactive && options.doubleClickZoom) map.doubleClickZoom.enable(); 22797 const tapDragZoom = new TapDragZoomHandler(); 22798 this._add("tapDragZoom", tapDragZoom); 22799 const touchPitch = map.touchPitch = new TwoFingersTouchPitchHandler(map); 22800 this._add("touchPitch", touchPitch); 22801 if (options.interactive && options.touchPitch) map.touchPitch.enable(options.touchPitch); 22802 const getCenter = () => this._camera.transform.centerPoint; 22803 const mouseRotate = generateMouseRotationHandler(options, getCenter); 22804 const mousePitch = generateMousePitchHandler(options); 22805 const mouseRoll = generateMouseRollHandler(options, getCenter); 22806 map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch, mouseRoll); 22807 this._add("mouseRotate", mouseRotate, ["mousePitch"]); 22808 this._add("mousePitch", mousePitch, ["mouseRotate", "mouseRoll"]); 22809 this._add("mouseRoll", mouseRoll, ["mousePitch"]); 22810 if (options.interactive && options.dragRotate) map.dragRotate.enable(); 22811 const mousePan = generateMousePanHandler(options); 22812 const touchPan = new TouchPanHandler(options, map); 22813 map.dragPan = new DragPanHandler(el, mousePan, touchPan); 22814 this._add("mousePan", mousePan); 22815 this._add("touchPan", touchPan, ["touchZoom", "touchRotate"]); 22816 if (options.interactive && options.dragPan) map.dragPan.enable(options.dragPan); 22817 const touchRotate = new TwoFingersTouchRotateHandler(); 22818 const touchZoom = new TwoFingersTouchZoomHandler(); 22819 map.touchZoomRotate = new TwoFingersTouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); 22820 this._add("touchRotate", touchRotate, ["touchPan", "touchZoom"]); 22821 this._add("touchZoom", touchZoom, ["touchPan", "touchRotate"]); 22822 if (options.interactive && options.touchZoomRotate) map.touchZoomRotate.enable(options.touchZoomRotate); 22823 this._add("blockableMapEvent", new BlockableMapEventHandler(map, options), ["touchPan"]); 22824 const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, () => this._triggerRenderFrame(), this._transformProvider); 22825 this._add("scrollZoom", scrollZoom, ["mousePan"]); 22826 if (options.interactive && options.scrollZoom) map.scrollZoom.enable(options.scrollZoom); 22827 const keyboard = map.keyboard = new KeyboardHandler(map, this._transformProvider); 22828 this._add("keyboard", keyboard); 22829 if (options.interactive && options.keyboard) map.keyboard.enable(); 22830 } 22831 _add(handlerName, handler, allowed) { 22832 this._handlers.push({ 22833 handlerName, 22834 handler, 22835 allowed 22836 }); 22837 this._handlersById[handlerName] = handler; 22838 } 22839 stop(allowEndAnimation) { 22840 if (this._updatingCamera) return; 22841 for (const { handler } of this._handlers) handler.reset(); 22842 this._inertia.clear(); 22843 this._fireEvents({}, {}, allowEndAnimation); 22844 this._changes = []; 22845 } 22846 isActive() { 22847 for (const { handler } of this._handlers) if (handler.isActive()) return true; 22848 return false; 22849 } 22850 isZooming() { 22851 return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); 22852 } 22853 isRotating() { 22854 return !!this._eventsInProgress.rotate; 22855 } 22856 isMoving() { 22857 return Boolean(isMoving(this._eventsInProgress)) || this.isZooming(); 22858 } 22859 _blockedByActive(activeHandlers, allowed, myName) { 22860 for (const name in activeHandlers) { 22861 if (name === myName) continue; 22862 if (!allowed?.includes(name)) return true; 22863 } 22864 return false; 22865 } 22866 _getMapTouches(touches) { 22867 const mapTouches = []; 22868 for (const t of touches) { 22869 const target = t.target; 22870 if (this._el.contains(target)) mapTouches.push(t); 22871 } 22872 return mapTouches; 22873 } 22874 mergeHandlerResult(mergedHandlerResult, eventsInProgress, handlerResult, name, e) { 22875 if (!handlerResult) return; 22876 extend(mergedHandlerResult, handlerResult); 22877 const eventData = { 22878 handlerName: name, 22879 originalEvent: handlerResult.originalEvent || e 22880 }; 22881 if (handlerResult.zoomDelta !== void 0) eventsInProgress.zoom = eventData; 22882 if (handlerResult.panDelta !== void 0) eventsInProgress.drag = eventData; 22883 if (handlerResult.rollDelta !== void 0) eventsInProgress.roll = eventData; 22884 if (handlerResult.pitchDelta !== void 0) eventsInProgress.pitch = eventData; 22885 if (handlerResult.bearingDelta !== void 0) eventsInProgress.rotate = eventData; 22886 } 22887 _applyChanges() { 22888 const combined = {}; 22889 const combinedEventsInProgress = {}; 22890 const combinedDeactivatedHandlers = {}; 22891 for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) { 22892 if (change.panDelta) combined.panDelta = (combined.panDelta || new Point(0, 0))._add(change.panDelta); 22893 if (change.zoomDelta) combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; 22894 if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; 22895 if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; 22896 if (change.rollDelta) combined.rollDelta = (combined.rollDelta || 0) + change.rollDelta; 22897 if (change.around !== void 0) combined.around = change.around; 22898 if (change.pinchAround !== void 0) combined.pinchAround = change.pinchAround; 22899 if (change.noInertia) combined.noInertia = change.noInertia; 22900 extend(combinedEventsInProgress, eventsInProgress); 22901 extend(combinedDeactivatedHandlers, deactivatedHandlers); 22902 } 22903 this._updateMapTransform(combined, combinedEventsInProgress, combinedDeactivatedHandlers); 22904 this._changes = []; 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 */ 22911 _updateMapTransform(combinedResult, combinedEventsInProgress, deactivatedHandlers) { 22912 const terrain = this._map.terrain; 22913 if (!hasChange(combinedResult)) { 22914 this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); 22915 return; 22916 } 22917 const tr = this._camera.getTransformForUpdate(); 22918 this._camera.stop(true); 22919 const { panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta } = combinedResult; 22920 let { around, aroundOnSurface } = this._resolveAround(combinedResult, terrain, tr); 22921 const aroundElevation = terrain ? this._terrainGestureElevation(terrain, around, aroundOnSurface, tr, combinedEventsInProgress) : void 0; 22922 const deltasForHelper = { 22923 panDelta, 22924 zoomDelta, 22925 rollDelta, 22926 pitchDelta, 22927 bearingDelta, 22928 around, 22929 aroundElevation 22930 }; 22931 if (this._camera.cameraHelper.useGlobeControls && !tr.isPointOnMapSurface(around)) around = tr.centerPoint; 22932 const preZoomAroundLoc = this._computePreZoomAroundLoc(tr, around, panDelta, aroundElevation); 22933 this._handleMapControls({ 22934 terrain, 22935 tr, 22936 deltasForHelper, 22937 preZoomAroundLoc, 22938 combinedEventsInProgress, 22939 panDelta 22940 }); 22941 this._camera.applyUpdatedTransform(tr); 22942 this._map._update(); 22943 if (!combinedResult.noInertia) this._inertia.record(combinedResult); 22944 this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); 22945 } 22946 /** 22947 * The gesture's anchor point: the pinch midpoint when pinching, otherwise the 22948 * pan's anchor, clamped to the center point when it does not lie on the map. 22949 */ 22950 _resolveAround(combinedResult, terrain, tr) { 22951 let around = combinedResult.pinchAround !== void 0 ? combinedResult.pinchAround : combinedResult.around; 22952 around ||= this._camera.transform.centerPoint; 22953 if (terrain && !tr.isPointOnMapSurface(around)) return { 22954 around: tr.centerPoint, 22955 aroundOnSurface: false 22956 }; 22957 return { 22958 around, 22959 aroundOnSurface: true 22960 }; 22961 } 22962 /** 22963 * The elevation of the plane a terrain gesture is solved on: the elevation of the 22964 * terrain point grabbed at gesture start, captured here on the gesture's first 22965 * frame. Undefined means the gesture is solved at the center's elevation instead â 22966 * when the grabbed terrain is not loaded, the anchor is the center point (a 22967 * center-point anchor is skipped by the camera helper and would lose the pan), or 22968 * the anchor plane is too close to the camera for a stable ray-plane solve. 22969 */ 22970 _terrainGestureElevation(terrain, around, aroundOnSurface, tr, combinedEventsInProgress) { 22971 if (!aroundOnSurface) return; 22972 if (this._terrainGesture.anchorElevation === null && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) { 22973 const anchor = tr.screenTerrainPointToMercatorCoordinate(around, terrain); 22974 this._terrainGesture.anchorElevation = anchor ? anchor.z : void 0; 22975 } 22976 const elevation = this._terrainGesture.anchorElevation; 22977 if (elevation === null || elevation === void 0) return; 22978 if (around.distSqr(tr.centerPoint) < .01) return; 22979 if (elevation - tr.elevation >= TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION * (tr.getCameraAltitude() - tr.elevation)) return; 22980 return elevation; 22981 } 22982 /** 22983 * The location that was under `around` before this frame's deltas â the point the 22984 * camera helper re-anchors after zooming â solved at `aroundElevation` when a 22985 * terrain gesture provides one. When rotating about the center point, the 22986 * transform's center is used directly to avoid numerical issues near the horizon. 22987 */ 22988 _computePreZoomAroundLoc(tr, around, panDelta, aroundElevation) { 22989 if (around.distSqr(tr.centerPoint) < .01) return tr.center; 22990 const aroundPreviousPoint = panDelta ? around.sub(panDelta) : around; 22991 if (aroundElevation !== void 0) return tr.screenPointToLocationAtElevation(aroundPreviousPoint, aroundElevation); 22992 return tr.screenPointToLocation(aroundPreviousPoint); 22993 } 22994 _handleMapControls({ terrain, tr, deltasForHelper, preZoomAroundLoc, combinedEventsInProgress, panDelta }) { 22995 const cameraHelper = this._camera.cameraHelper; 22996 cameraHelper.handleMapControlsRollPitchBearingZoom(deltasForHelper, tr); 22997 if (!terrain) { 22998 cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc); 22999 return; 23000 } 23001 if (!this._terrainGesture.inFlight) { 23002 this._terrainGesture.inFlight = true; 23003 this._camera.elevationFreeze = true; 23004 cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc); 23005 return; 23006 } 23007 if (!cameraHelper.useGlobeControls && deltasForHelper.aroundElevation === void 0 && combinedEventsInProgress.drag && panDelta) { 23008 tr.setCenter(tr.screenPointToLocation(tr.centerPoint.sub(panDelta))); 23009 return; 23010 } 23011 cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc); 23012 } 23013 _fireEvents(newEventsInProgress, deactivatedHandlers, allowEndAnimation) { 23014 const wasMoving = isMoving(this._eventsInProgress); 23015 const nowMoving = isMoving(newEventsInProgress); 23016 const startEvents = {}; 23017 for (const eventName in newEventsInProgress) { 23018 const { originalEvent } = newEventsInProgress[eventName]; 23019 if (!this._eventsInProgress[eventName]) startEvents[`${eventName}start`] = originalEvent; 23020 this._eventsInProgress[eventName] = newEventsInProgress[eventName]; 23021 } 23022 if (!wasMoving && nowMoving) this._fireEvent("movestart", nowMoving.originalEvent); 23023 for (const name in startEvents) this._fireEvent(name, startEvents[name]); 23024 if (nowMoving) this._fireEvent("move", nowMoving.originalEvent); 23025 for (const eventName in newEventsInProgress) { 23026 const { originalEvent } = newEventsInProgress[eventName]; 23027 this._fireEvent(eventName, originalEvent); 23028 } 23029 const endEvents = {}; 23030 let originalEndEvent; 23031 for (const eventName in this._eventsInProgress) { 23032 const { handlerName, originalEvent } = this._eventsInProgress[eventName]; 23033 if (!this._handlersById[handlerName].isActive()) { 23034 delete this._eventsInProgress[eventName]; 23035 originalEndEvent = deactivatedHandlers[handlerName] || originalEvent; 23036 endEvents[`${eventName}end`] = originalEndEvent; 23037 } 23038 } 23039 for (const name in endEvents) this._fireEvent(name, endEvents[name]); 23040 const stillMoving = isMoving(this._eventsInProgress); 23041 const finishedMoving = (wasMoving || nowMoving) && !stillMoving; 23042 if (finishedMoving && this._terrainGesture.inFlight) { 23043 this._camera.elevationFreeze = false; 23044 this._terrainGesture = { 23045 inFlight: false, 23046 anchorElevation: null 23047 }; 23048 const tr = this._camera.getTransformForUpdate(); 23049 if (this._map.getCenterClampedToGround()) tr.recalculateZoomAndCenter(this._map.terrain); 23050 this._camera.applyUpdatedTransform(tr); 23051 } 23052 if (allowEndAnimation && finishedMoving) { 23053 this._updatingCamera = true; 23054 const inertialEase = this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions); 23055 const shouldSnapToNorth = (bearing) => bearing !== 0 && -this._bearingSnap < bearing && bearing < this._bearingSnap; 23056 if (inertialEase && (inertialEase.essential || !browser.prefersReducedMotion)) { 23057 if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) inertialEase.bearing = 0; 23058 inertialEase.freezeElevation = true; 23059 this._map.easeTo(inertialEase, { originalEvent: originalEndEvent }); 23060 } else { 23061 this._fireEvent("moveend", originalEndEvent); 23062 if (shouldSnapToNorth(this._map.getBearing())) this._map.resetNorth(); 23063 } 23064 this._updatingCamera = false; 23065 } 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 */ 23072 _fireEvent(type, e) { 23073 this._camera.fire(new MapMovementEvent(type, e ? { originalEvent: e } : {})); 23074 } 23075 _requestFrame() { 23076 this._map.triggerRepaint(); 23077 return this._map._renderTaskQueue.add((timeStamp) => { 23078 delete this._frameId; 23079 this.handleEvent(new RenderFrameEvent("renderFrame", { timeStamp })); 23080 this._applyChanges(); 23081 }); 23082 } 23083 _triggerRenderFrame() { 23084 if (this._frameId === void 0) this._frameId = this._requestFrame(); 23085 } 23086}; 23087//#endregion 23088//#region src/ui/camera.ts 23089var Camera = class extends Evented { 23090 constructor(options) { 23091 super(); 23092 this._renderFrameCallback = () => { 23093 const t = Math.min((now() - this._easeStart) / this._easeOptions.duration, 1); 23094 this._onEaseFrame(this._easeOptions.easing(t)); 23095 if (t < 1 && this._easeFrameId) this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); 23096 else this.stop(); 23097 }; 23098 this.transform = new MercatorTransform(); 23099 this.cameraHelper = new MercatorCameraHelper(); 23100 if (options.minZoom !== void 0) this.transform.setMinZoom(options.minZoom); 23101 if (options.maxZoom !== void 0) this.transform.setMaxZoom(options.maxZoom); 23102 if (options.minPitch !== void 0) this.transform.setMinPitch(options.minPitch); 23103 if (options.maxPitch !== void 0) this.transform.setMaxPitch(options.maxPitch); 23104 if (options.renderWorldCopies !== void 0) this.transform.setRenderWorldCopies(options.renderWorldCopies); 23105 if (options.transformConstrain !== null) this.transform.setConstrainOverride(options.transformConstrain); 23106 this._moving = false; 23107 this._zooming = false; 23108 this._bearingSnap = options.bearingSnap; 23109 this._zoomSnap = options.zoomSnap; 23110 this._requestRenderFrame = options.requestRenderFrame; 23111 this._cancelRenderFrame = options.cancelRenderFrame; 23112 this.terrain = options.terrain; 23113 this._centerClampedToGround = options.centerClampedToGround ?? true; 23114 this.transformCameraUpdate = options.transformCameraUpdate ?? null; 23115 this._stopHandlers = options.stopHandlers ?? (() => {}); 23116 this.on("moveend", () => { 23117 delete this._requestedCameraState; 23118 }); 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 } 23129 migrateProjection(newTransform, newCameraHelper) { 23130 newTransform.apply(this.transform, true); 23131 this.transform = newTransform; 23132 this.cameraHelper = newCameraHelper; 23133 if (this._requestedCameraState) { 23134 const requestedCameraState = newTransform.clone(); 23135 requestedCameraState.apply(this._requestedCameraState, true); 23136 this._requestedCameraState = requestedCameraState; 23137 } 23138 } 23139 getCenter() { 23140 return new LngLat(this.transform.center.lng, this.transform.center.lat); 23141 } 23142 setCenter(center, eventData) { 23143 return this.jumpTo({ center }, eventData); 23144 } 23145 getCenterElevation() { 23146 return this.transform.elevation; 23147 } 23148 setCenterElevation(elevation, eventData) { 23149 this.jumpTo({ elevation }, eventData); 23150 return this; 23151 } 23152 getCenterClampedToGround() { 23153 return this._centerClampedToGround; 23154 } 23155 setCenterClampedToGround(centerClampedToGround) { 23156 this._centerClampedToGround = centerClampedToGround; 23157 } 23158 panBy(offset, options, eventData) { 23159 offset = Point.convert(offset).mult(-1); 23160 return this.panTo(this.transform.center, extend({ offset }, options), eventData); 23161 } 23162 panTo(lnglat, options, eventData) { 23163 return this.easeTo(extend({ center: lnglat }, options), eventData); 23164 } 23165 getZoom() { 23166 return this.transform.zoom; 23167 } 23168 setZoom(zoom, eventData) { 23169 this.jumpTo({ zoom }, eventData); 23170 return this; 23171 } 23172 zoomTo(zoom, options, eventData) { 23173 return this.easeTo(extend({ zoom }, options), eventData); 23174 } 23175 zoomIn(options, eventData) { 23176 this.zoomTo(evaluateZoomSnap(this.getZoom() + 1, this._zoomSnap), options, eventData); 23177 return this; 23178 } 23179 zoomOut(options, eventData) { 23180 this.zoomTo(evaluateZoomSnap(this.getZoom() - 1, this._zoomSnap), options, eventData); 23181 return this; 23182 } 23183 getVerticalFieldOfView() { 23184 return this.transform.fov; 23185 } 23186 setVerticalFieldOfView(fov, eventData) { 23187 if (fov != this.transform.fov) { 23188 this.transform.setFov(fov); 23189 this.fire(new MapMovementEvent("movestart", eventData)).fire(new MapMovementEvent("move", eventData)).fire(new MapMovementEvent("moveend", eventData)); 23190 } 23191 return this; 23192 } 23193 getBearing() { 23194 return this.transform.bearing; 23195 } 23196 setZoomSnap(snap) { 23197 this._zoomSnap = snap; 23198 return this; 23199 } 23200 getZoomSnap() { 23201 return this._zoomSnap; 23202 } 23203 setBearing(bearing, eventData) { 23204 this.jumpTo({ bearing }, eventData); 23205 return this; 23206 } 23207 getPadding() { 23208 return this.transform.padding; 23209 } 23210 setPadding(padding, eventData) { 23211 this.jumpTo({ padding }, eventData); 23212 return this; 23213 } 23214 rotateTo(bearing, options, eventData) { 23215 return this.easeTo(extend({ bearing }, options), eventData); 23216 } 23217 resetNorth(options, eventData) { 23218 this.rotateTo(0, extend({ duration: 1e3 }, options), eventData); 23219 return this; 23220 } 23221 resetNorthPitch(options, eventData) { 23222 this.easeTo(extend({ 23223 bearing: 0, 23224 pitch: 0, 23225 roll: 0, 23226 duration: 1e3 23227 }, options), eventData); 23228 return this; 23229 } 23230 snapToNorth(options, eventData) { 23231 if (Math.abs(this.getBearing()) < this._bearingSnap) return this.resetNorth(options, eventData); 23232 return this; 23233 } 23234 getPitch() { 23235 return this.transform.pitch; 23236 } 23237 setPitch(pitch, eventData) { 23238 this.jumpTo({ pitch }, eventData); 23239 return this; 23240 } 23241 getRoll() { 23242 return this.transform.roll; 23243 } 23244 setRoll(roll, eventData) { 23245 this.jumpTo({ roll }, eventData); 23246 return this; 23247 } 23248 cameraForBounds(bounds, options) { 23249 bounds = LngLatBounds.convert(bounds).adjustAntiMeridian(); 23250 const bearing = options?.bearing || 0; 23251 return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), bearing, options); 23252 } 23253 /** 23254 * @internal 23255 * Calculate the center of these two points in the viewport and use 23256 * the highest zoom level up to and including {@link Map.getMaxZoom} that fits 23257 * the AABB defined by these points in the viewport at the specified bearing. 23258 * @param p0 - First point 23259 * @param p1 - Second point 23260 * @param bearing - Desired map bearing at end of animation, in degrees 23261 * @param options - the camera options 23262 * @returns If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`. 23263 * If map is unable to fit, method will warn and return undefined. 23264 * @example 23265 * ```ts 23266 * let p0 = [-79, 43]; 23267 * let p1 = [-73, 45]; 23268 * let bearing = 90; 23269 * let newCameraTransform = map._cameraForBoxAndBearing(p0, p1, bearing, { 23270 * padding: {top: 10, bottom:25, left: 15, right: 5} 23271 * }); 23272 * ``` 23273 */ 23274 _cameraForBoxAndBearing(p0, p1, bearing, options) { 23275 const defaultPadding = { 23276 top: 0, 23277 bottom: 0, 23278 right: 0, 23279 left: 0 23280 }; 23281 options = extend({ 23282 padding: defaultPadding, 23283 offset: [0, 0], 23284 maxZoom: this.transform.maxZoom 23285 }, options); 23286 if (typeof options.padding === "number") { 23287 const p = options.padding; 23288 options.padding = { 23289 top: p, 23290 bottom: p, 23291 right: p, 23292 left: p 23293 }; 23294 } 23295 const padding = extend(defaultPadding, options.padding); 23296 options.padding = padding; 23297 const tr = this.transform; 23298 const bounds = new LngLatBounds(p0, p1); 23299 const result = this.cameraHelper.cameraForBoxAndBearing(options, padding, bounds, bearing, tr); 23300 if (result && this._zoomSnap) result.zoom = evaluateZoomSnap(result.zoom, this._zoomSnap, -1); 23301 return result; 23302 } 23303 fitBounds(bounds, options, eventData) { 23304 return this._fitInternal(this.cameraForBounds(bounds, options), options, eventData); 23305 } 23306 fitScreenCoordinates(p0, p1, bearing, options, eventData) { 23307 return this._fitInternal(this._cameraForBoxAndBearing(this.transform.screenPointToLocation(Point.convert(p0)), this.transform.screenPointToLocation(Point.convert(p1)), bearing, options), options, eventData); 23308 } 23309 _fitInternal(calculatedOptions, options, eventData) { 23310 if (!calculatedOptions) return this; 23311 options = extend(calculatedOptions, options); 23312 delete options.padding; 23313 return options.linear ? this.easeTo(options, eventData) : this.flyTo(options, eventData); 23314 } 23315 jumpTo(options, eventData) { 23316 this.stop(); 23317 if (options.zoom !== void 0 && this._zoomSnap) options.zoom = evaluateZoomSnap(options.zoom, this._zoomSnap); 23318 const tr = this.getTransformForUpdate(); 23319 let bearingChanged = false, pitchChanged = false; 23320 let rollChanged = false; 23321 const oldZoom = tr.zoom; 23322 if (this.terrain) tr.setElevation(this.terrain.getElevationForLngLat(options.center ? LngLat.convert(options.center) : tr.center, tr)); 23323 this.cameraHelper.handleJumpToCenterZoom(tr, options); 23324 const zoomChanged = tr.zoom !== oldZoom; 23325 if (options.elevation !== void 0 && tr.elevation !== +options.elevation) tr.setElevation(+options.elevation); 23326 if (options.bearing !== void 0 && tr.bearing !== +options.bearing) { 23327 bearingChanged = true; 23328 tr.setBearing(+options.bearing); 23329 } 23330 if (options.pitch !== void 0 && tr.pitch !== +options.pitch) { 23331 pitchChanged = true; 23332 tr.setPitch(+options.pitch); 23333 } 23334 if (options.roll !== void 0 && tr.roll !== +options.roll) { 23335 rollChanged = true; 23336 tr.setRoll(+options.roll); 23337 } 23338 if (options.padding != null && !tr.isPaddingEqual(options.padding)) tr.setPadding(options.padding); 23339 this.applyUpdatedTransform(tr); 23340 this.fire(new MapMovementEvent("movestart", eventData)).fire(new MapMovementEvent("move", eventData)); 23341 if (zoomChanged) this.fire(new MapMovementEvent("zoomstart", eventData)).fire(new MapMovementEvent("zoom", eventData)).fire(new MapMovementEvent("zoomend", eventData)); 23342 if (bearingChanged) this.fire(new MapMovementEvent("rotatestart", eventData)).fire(new MapMovementEvent("rotate", eventData)).fire(new MapMovementEvent("rotateend", eventData)); 23343 if (pitchChanged) this.fire(new MapMovementEvent("pitchstart", eventData)).fire(new MapMovementEvent("pitch", eventData)).fire(new MapMovementEvent("pitchend", eventData)); 23344 if (rollChanged) this.fire(new MapMovementEvent("rollstart", eventData)).fire(new MapMovementEvent("roll", eventData)).fire(new MapMovementEvent("rollend", eventData)); 23345 return this.fire(new MapMovementEvent("moveend", eventData)); 23346 } 23347 /** 23348 * Calculates camera options for moving a geographic anchor to a screen point without 23349 * changing this camera. 23350 */ 23351 calculateAnchoredCameraOptions(options) { 23352 const tr = this.transform.clone(); 23353 const anchor = LngLat.convert(options.anchorLocation); 23354 const target = Point.convert(options.anchorScreenPoint); 23355 const deltas = { 23356 panDelta: target.sub(this.transform.locationToScreenPoint(anchor, this.terrain)), 23357 zoomDelta: options.zoom === void 0 ? 0 : options.zoom - tr.zoom, 23358 bearingDelta: 0, 23359 pitchDelta: 0, 23360 rollDelta: 0, 23361 around: target, 23362 aroundElevation: this.terrain?.getElevationForLngLat(anchor, tr) 23363 }; 23364 this.cameraHelper.handleMapControlsRollPitchBearingZoom(deltas, tr); 23365 this.cameraHelper.handleMapControlsPan(deltas, tr, anchor); 23366 return { 23367 center: tr.center, 23368 zoom: tr.zoom 23369 }; 23370 } 23371 calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat, cameraAlt, bearing, pitch, roll) { 23372 const centerInfo = this.transform.calculateCenterFromCameraLngLatAlt(cameraLngLat, cameraAlt, bearing, pitch); 23373 return { 23374 center: centerInfo.center, 23375 elevation: centerInfo.elevation, 23376 zoom: centerInfo.zoom, 23377 bearing, 23378 pitch, 23379 roll 23380 }; 23381 } 23382 easeTo(options, eventData) { 23383 this._stop(false, options.easeId); 23384 options = extend({ 23385 offset: [0, 0], 23386 duration: 500, 23387 easing: defaultEasing 23388 }, options); 23389 if (options.zoom !== void 0 && this._zoomSnap) options.zoom = evaluateZoomSnap(options.zoom, this._zoomSnap); 23390 if (options.animate === false || !options.essential && browser.prefersReducedMotion) options.duration = 0; 23391 const tr = this.getTransformForUpdate(); 23392 const startBearing = this.getBearing(), startPitch = tr.pitch, startRoll = tr.roll, bearing = options.bearing !== void 0 ? this._normalizeBearing(options.bearing, startBearing) : startBearing, pitch = options.pitch !== void 0 ? +options.pitch : startPitch, roll = options.roll !== void 0 ? this._normalizeBearing(options.roll, startRoll) : startRoll, padding = options.padding !== void 0 ? options.padding : tr.padding; 23393 const offsetAsPoint = Point.convert(options.offset); 23394 let around, aroundPoint; 23395 if (options.around) { 23396 around = LngLat.convert(options.around); 23397 aroundPoint = tr.locationToScreenPoint(around); 23398 } 23399 const currently = { 23400 moving: this._moving, 23401 zooming: this._zooming, 23402 rotating: this._rotating, 23403 pitching: this._pitching, 23404 rolling: this._rolling 23405 }; 23406 const easeHandler = this.cameraHelper.handleEaseTo(tr, { 23407 bearing, 23408 pitch, 23409 roll, 23410 padding, 23411 around, 23412 aroundPoint, 23413 offsetAsPoint, 23414 offset: options.offset, 23415 zoom: options.zoom, 23416 center: options.center 23417 }); 23418 this._rotating ||= startBearing !== bearing; 23419 this._pitching ||= pitch !== startPitch; 23420 this._rolling ||= roll !== startRoll; 23421 this._padding = !tr.isPaddingEqual(padding); 23422 this._zooming ||= easeHandler.isZooming; 23423 this._easeId = options.easeId; 23424 this._prepareEase(eventData, options.noMoveStart, currently); 23425 if (this.terrain) this._prepareElevation(easeHandler.elevationCenter); 23426 this._ease((k) => { 23427 easeHandler.easeFunc(k); 23428 if (this.terrain && !options.freezeElevation) this._updateElevation(k); 23429 this.applyUpdatedTransform(tr); 23430 this._fireMoveEvents(eventData); 23431 }, (interruptingEaseId) => { 23432 if (this.terrain && options.freezeElevation) this._finalizeElevation(); 23433 else this.elevationFreeze = false; 23434 this._afterEase(eventData, interruptingEaseId); 23435 }, options); 23436 return this; 23437 } 23438 _prepareEase(eventData, noMoveStart, currently = {}) { 23439 this._moving = true; 23440 if (!noMoveStart && !currently.moving) this.fire(new MapMovementEvent("movestart", eventData)); 23441 if (this._zooming && !currently.zooming) this.fire(new MapMovementEvent("zoomstart", eventData)); 23442 if (this._rotating && !currently.rotating) this.fire(new MapMovementEvent("rotatestart", eventData)); 23443 if (this._pitching && !currently.pitching) this.fire(new MapMovementEvent("pitchstart", eventData)); 23444 if (this._rolling && !currently.rolling) this.fire(new MapMovementEvent("rollstart", eventData)); 23445 } 23446 _prepareElevation(center) { 23447 this._elevationCenter = center; 23448 this._elevationStart = this.transform.elevation; 23449 this._elevationTarget = this.terrain.getElevationForLngLat(center, this.transform); 23450 this.elevationFreeze = true; 23451 } 23452 _updateElevation(k) { 23453 if (this._elevationStart === void 0 || this._elevationCenter === void 0) this._prepareElevation(this.transform.center); 23454 this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter, this.transform.tileZoom)); 23455 const elevation = this.terrain.getElevationForLngLat(this._elevationCenter, this.transform); 23456 if (k < 1 && elevation !== this._elevationTarget) { 23457 const pitch1 = this._elevationTarget - this._elevationStart; 23458 const pitch2 = (elevation - (pitch1 * k + this._elevationStart)) / (1 - k); 23459 this._elevationStart += k * (pitch1 - pitch2); 23460 this._elevationTarget = elevation; 23461 } 23462 this.transform.setElevation(interpolateFactory.number(this._elevationStart, this._elevationTarget, k)); 23463 } 23464 _finalizeElevation() { 23465 this.elevationFreeze = false; 23466 if (this.getCenterClampedToGround()) this.transform.recalculateZoomAndCenter(this.terrain); 23467 } 23468 /** 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 23487 * Called when the camera is about to be manipulated. 23488 * If `transformCameraUpdate` is specified or terrain is enabled, a copy of 23489 * the current transform is created to track the accumulated changes. 23490 * This underlying transform represents the "desired state" proposed by input handlers / animations / UI controls. 23491 * It may differ from the state used for rendering (`this.transform`). 23492 * @returns Transform to apply changes to 23493 */ 23494 getTransformForUpdate() { 23495 if (!this.transformCameraUpdate && !this.terrain) return this.transform; 23496 this._requestedCameraState ||= this.transform.clone(); 23497 return this._requestedCameraState; 23498 } 23499 /** 23500 * @internal 23501 * Checks the given transform for the camera being below terrain surface and 23502 * returns new pitch and zoom to fix that. 23503 * 23504 * With the new pitch and zoom, the camera will be at the same ground 23505 * position but at higher altitude. It will still point to the same spot on 23506 * the map. 23507 * 23508 * @param tr - The transform to check. 23509 */ 23510 _elevateCameraIfInsideTerrain(tr) { 23511 if (!this.terrain && tr.elevation >= 0 && tr.pitch <= 90) return {}; 23512 const cameraLngLat = tr.getCameraLngLat(); 23513 const cameraAltitude = tr.getCameraAltitude(); 23514 const minAltitude = this.terrain ? this.terrain.getElevationForLngLatZoom(cameraLngLat, tr.zoom) : 0; 23515 if (cameraAltitude < minAltitude) { 23516 const newCamera = tr.calculateCameraOptionsFromTo(cameraLngLat, minAltitude, tr.center, tr.elevation); 23517 return { 23518 pitch: newCamera.pitch, 23519 zoom: newCamera.zoom 23520 }; 23521 } 23522 return {}; 23523 } 23524 /** 23525 * @internal 23526 * Called after the camera is done being manipulated. 23527 * @param tr - the requested camera end state 23528 * If the camera is inside terrain, it gets elevated. 23529 * Call `transformCameraUpdate` if present, and then apply the "approved" changes. 23530 */ 23531 applyUpdatedTransform(tr) { 23532 const modifiers = []; 23533 modifiers.push((tr) => this._elevateCameraIfInsideTerrain(tr)); 23534 if (this.transformCameraUpdate) modifiers.push((tr) => this.transformCameraUpdate(tr)); 23535 if (!modifiers.length) return; 23536 const finalTransform = tr.clone(); 23537 for (const modifier of modifiers) { 23538 const nextTransform = finalTransform.clone(); 23539 const { center, zoom, roll, pitch, bearing, elevation } = modifier(nextTransform); 23540 if (center) nextTransform.setCenter(center); 23541 if (elevation !== void 0) nextTransform.setElevation(elevation); 23542 if (zoom !== void 0) nextTransform.setZoom(zoom); 23543 if (roll !== void 0) nextTransform.setRoll(roll); 23544 if (pitch !== void 0) nextTransform.setPitch(pitch); 23545 if (bearing !== void 0) nextTransform.setBearing(bearing); 23546 finalTransform.apply(nextTransform, false); 23547 } 23548 this.transform.apply(finalTransform, false); 23549 } 23550 _fireMoveEvents(eventData) { 23551 this.fire(new MapMovementEvent("move", eventData)); 23552 if (this._zooming) this.fire(new MapMovementEvent("zoom", eventData)); 23553 if (this._rotating) this.fire(new MapMovementEvent("rotate", eventData)); 23554 if (this._pitching) this.fire(new MapMovementEvent("pitch", eventData)); 23555 if (this._rolling) this.fire(new MapMovementEvent("roll", eventData)); 23556 } 23557 _afterEase(eventData, easeId) { 23558 if (this._easeId && easeId && this._easeId === easeId) return; 23559 delete this._easeId; 23560 const wasZooming = this._zooming; 23561 const wasRotating = this._rotating; 23562 const wasPitching = this._pitching; 23563 const wasRolling = this._rolling; 23564 this._moving = false; 23565 this._zooming = false; 23566 this._rotating = false; 23567 this._pitching = false; 23568 this._rolling = false; 23569 this._padding = false; 23570 if (wasZooming) this.fire(new MapMovementEvent("zoomend", eventData)); 23571 if (wasRotating) this.fire(new MapMovementEvent("rotateend", eventData)); 23572 if (wasPitching) this.fire(new MapMovementEvent("pitchend", eventData)); 23573 if (wasRolling) this.fire(new MapMovementEvent("rollend", eventData)); 23574 this.fire(new MapMovementEvent("moveend", eventData)); 23575 } 23576 flyTo(options, eventData) { 23577 if (!options.essential && browser.prefersReducedMotion) { 23578 const coercedOptions = pick(options, [ 23579 "center", 23580 "zoom", 23581 "bearing", 23582 "pitch", 23583 "roll", 23584 "elevation", 23585 "padding" 23586 ]); 23587 return this.jumpTo(coercedOptions, eventData); 23588 } 23589 this.stop(); 23590 options = extend({ 23591 offset: [0, 0], 23592 speed: 1.2, 23593 curve: 1.42, 23594 easing: defaultEasing 23595 }, options); 23596 if (options.zoom !== void 0 && this._zoomSnap) options.zoom = evaluateZoomSnap(options.zoom, this._zoomSnap); 23597 const tr = this.getTransformForUpdate(), startBearing = tr.bearing, startPitch = tr.pitch, startRoll = tr.roll, startPadding = tr.padding; 23598 const bearing = options.bearing !== void 0 ? this._normalizeBearing(options.bearing, startBearing) : startBearing; 23599 const pitch = options.pitch !== void 0 ? +options.pitch : startPitch; 23600 const roll = options.roll !== void 0 ? this._normalizeBearing(options.roll, startRoll) : startRoll; 23601 const padding = options.padding !== void 0 ? options.padding : tr.padding; 23602 const offsetAsPoint = Point.convert(options.offset); 23603 let pointAtOffset = tr.centerPoint.add(offsetAsPoint); 23604 const locationAtOffset = tr.screenPointToLocation(pointAtOffset); 23605 const flyToHandler = this.cameraHelper.handleFlyTo(tr, { 23606 bearing, 23607 pitch, 23608 roll, 23609 padding, 23610 locationAtOffset, 23611 offsetAsPoint, 23612 center: options.center, 23613 minZoom: options.minZoom, 23614 zoom: options.zoom 23615 }); 23616 let rho = options.curve; 23617 const w0 = Math.max(tr.width, tr.height); 23618 const w1 = w0 / flyToHandler.scaleOfZoom; 23619 const u1 = flyToHandler.pixelPathLength; 23620 const wMax = w0 / flyToHandler.scaleOfMinZoom; 23621 rho = Math.min(rho, Math.sqrt(wMax / u1 * 2)); 23622 const rho2 = rho * rho; 23623 /** 23624 * ráµ¢: Returns the zoom-out factor at one end of the animation. 23625 * 23626 * @param descent - `true` for the descent, `false` for the ascent 23627 */ 23628 function zoomOutFactor(descent) { 23629 const b = (w1 * w1 - w0 * w0 + (descent ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (descent ? w1 : w0) * rho2 * u1); 23630 return Math.log(Math.sqrt(b * b + 1) - b); 23631 } 23632 function sinh(n) { 23633 return (Math.exp(n) - Math.exp(-n)) / 2; 23634 } 23635 function cosh(n) { 23636 return (Math.exp(n) + Math.exp(-n)) / 2; 23637 } 23638 function tanh(n) { 23639 return sinh(n) / cosh(n); 23640 } 23641 const r0 = zoomOutFactor(false); 23642 let w = function(s) { 23643 return cosh(r0) / cosh(r0 + rho * s); 23644 }; 23645 let u = function(s) { 23646 return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; 23647 }; 23648 let S = (zoomOutFactor(true) - r0) / rho; 23649 if (Math.abs(u1) < 2e-6 || !isFinite(S)) { 23650 if (Math.abs(w0 - w1) < 1e-6) return this.easeTo(options, eventData); 23651 const k = w1 < w0 ? -1 : 1; 23652 S = Math.abs(Math.log(w1 / w0)) / rho; 23653 u = () => 0; 23654 w = (s) => Math.exp(k * rho * s); 23655 } 23656 if (options.duration !== void 0) options.duration = +options.duration; 23657 else { 23658 const V = options.screenSpeed !== void 0 ? +options.screenSpeed / rho : +options.speed; 23659 options.duration = 1e3 * S / V; 23660 } 23661 if (options.maxDuration && options.duration > options.maxDuration) options.duration = 0; 23662 this._zooming = true; 23663 this._rotating = startBearing !== bearing; 23664 this._pitching = pitch !== startPitch; 23665 this._rolling = roll !== startRoll; 23666 this._padding = !tr.isPaddingEqual(padding); 23667 this._prepareEase(eventData, false); 23668 if (this.terrain) this._prepareElevation(flyToHandler.targetCenter); 23669 this._ease((k) => { 23670 const s = k * S; 23671 const scale = 1 / w(s); 23672 const centerFactor = u(s); 23673 if (this._rotating) tr.setBearing(interpolateFactory.number(startBearing, bearing, k)); 23674 if (this._pitching) tr.setPitch(interpolateFactory.number(startPitch, pitch, k)); 23675 if (this._rolling) tr.setRoll(interpolateFactory.number(startRoll, roll, k)); 23676 if (this._padding) { 23677 tr.interpolatePadding(startPadding, padding, k); 23678 pointAtOffset = tr.centerPoint.add(offsetAsPoint); 23679 } 23680 flyToHandler.easeFunc(k, scale, centerFactor, pointAtOffset); 23681 if (this.terrain && !options.freezeElevation) this._updateElevation(k); 23682 this.applyUpdatedTransform(tr); 23683 this._fireMoveEvents(eventData); 23684 }, () => { 23685 if (this.terrain && options.freezeElevation) this._finalizeElevation(); 23686 else this.elevationFreeze = false; 23687 this._afterEase(eventData); 23688 }, options); 23689 return this; 23690 } 23691 isEasing() { 23692 return !!this._easeFrameId; 23693 } 23694 stop(allowGestures) { 23695 return this._stop(allowGestures); 23696 } 23697 _stop(allowGestures, easeId) { 23698 if (this._easeFrameId) { 23699 this._cancelRenderFrame(this._easeFrameId); 23700 delete this._easeFrameId; 23701 delete this._onEaseFrame; 23702 } 23703 if (this._onEaseEnd) { 23704 const onEaseEnd = this._onEaseEnd; 23705 delete this._onEaseEnd; 23706 onEaseEnd.call(this, easeId); 23707 } 23708 if (!allowGestures) this._stopHandlers(); 23709 return this; 23710 } 23711 _ease(frame, finish, options) { 23712 if (options.animate === false || options.duration === 0) { 23713 frame(1); 23714 finish(); 23715 } else { 23716 this._easeStart = now(); 23717 this._easeOptions = options; 23718 this._onEaseFrame = frame; 23719 this._onEaseEnd = finish; 23720 this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); 23721 } 23722 } 23723 _normalizeBearing(bearing, currentBearing) { 23724 bearing = wrap(bearing, -180, 180); 23725 const diff = Math.abs(bearing - currentBearing); 23726 if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360; 23727 if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360; 23728 return bearing; 23729 } 23730 isMoving() { 23731 return this._moving; 23732 } 23733 isZooming() { 23734 return this._zooming; 23735 } 23736 isRotating() { 23737 return this._rotating; 23738 } 23739}; 23740//#endregion 23741//#region src/ui/control/attribution_control.ts 23742const defaultAttributionControlOptions = { 23743 compact: true, 23744 customAttribution: "<a href=\"https://maplibre.org/\" target=\"_blank\">MapLibre</a>" 23745}; 23746/** 23747* An `AttributionControl` control presents the map's attribution information. By default, the attribution control is expanded (regardless of map width). 23748* @group Markers and Controls 23749* @example 23750* ```ts 23751* let map = new Map({attributionControl: false}) 23752* .addControl(new AttributionControl({ 23753* compact: true 23754* })); 23755* ``` 23756* @see [Change the default position for attribution](https://maplibre.org/maplibre-gl-js/docs/examples/change-the-default-position-for-attribution/) 23757*/ 23758var AttributionControl = class { 23759 /** 23760 * @param options - the attribution options 23761 */ 23762 constructor(options = defaultAttributionControlOptions) { 23763 this._toggleAttribution = () => { 23764 if (this._container.classList.contains("maplibregl-compact")) { 23765 if (this._container.classList.contains("maplibregl-compact-show")) { 23766 this._container.setAttribute("open", ""); 23767 this._container.classList.remove("maplibregl-compact-show"); 23768 } else { 23769 this._container.classList.add("maplibregl-compact-show"); 23770 this._container.removeAttribute("open"); 23771 } 23772 } 23773 }; 23774 this._updateData = (e) => { 23775 if (e && (e.type === "terrain" || e.dataType === "style" || e.dataType === "source" && (e.sourceDataType === "metadata" || e.sourceDataType === "visibility"))) this._updateAttributions(); 23776 }; 23777 this._updateCompact = () => { 23778 if (this._map.getCanvasContainer().offsetWidth <= 640 || this._compact) { 23779 if (this._compact === false) this._container.setAttribute("open", ""); 23780 else if (!this._container.classList.contains("maplibregl-compact") && !this._container.classList.contains("maplibregl-attrib-empty")) { 23781 this._container.setAttribute("open", ""); 23782 this._container.classList.add("maplibregl-compact", "maplibregl-compact-show"); 23783 } 23784 } else { 23785 this._container.setAttribute("open", ""); 23786 if (this._container.classList.contains("maplibregl-compact")) this._container.classList.remove("maplibregl-compact", "maplibregl-compact-show"); 23787 } 23788 }; 23789 this._updateCompactMinimize = () => { 23790 if (this._container.classList.contains("maplibregl-compact")) { 23791 if (this._container.classList.contains("maplibregl-compact-show")) this._container.classList.remove("maplibregl-compact-show"); 23792 } 23793 }; 23794 this.options = options; 23795 } 23796 getDefaultPosition() { 23797 return "bottom-right"; 23798 } 23799 /** {@inheritDoc IControl.onAdd} */ 23800 onAdd(map) { 23801 this._map = map; 23802 this._compact = this.options.compact; 23803 this._container = DOM.create("details", "maplibregl-ctrl maplibregl-ctrl-attrib"); 23804 this._compactButton = DOM.create("summary", "maplibregl-ctrl-attrib-button", this._container); 23805 this._compactButton.addEventListener("click", this._toggleAttribution); 23806 this._setElementTitle(this._compactButton, "ToggleAttribution"); 23807 this._innerContainer = DOM.create("div", "maplibregl-ctrl-attrib-inner", this._container); 23808 this._updateAttributions(); 23809 this._updateCompact(); 23810 this._map.on("styledata", this._updateData); 23811 this._map.on("sourcedata", this._updateData); 23812 this._map.on("terrain", this._updateData); 23813 this._map.on("resize", this._updateCompact); 23814 this._map.on("drag", this._updateCompactMinimize); 23815 return this._container; 23816 } 23817 /** {@inheritDoc IControl.onRemove} */ 23818 onRemove() { 23819 this._container.remove(); 23820 this._map.off("styledata", this._updateData); 23821 this._map.off("sourcedata", this._updateData); 23822 this._map.off("terrain", this._updateData); 23823 this._map.off("resize", this._updateCompact); 23824 this._map.off("drag", this._updateCompactMinimize); 23825 this._map = void 0; 23826 this._compact = void 0; 23827 this._attribHTML = void 0; 23828 } 23829 _setElementTitle(element, title) { 23830 const str = this._map._getUIString(`AttributionControl.${title}`); 23831 element.title = str; 23832 element.setAttribute("aria-label", str); 23833 } 23834 _updateAttributions() { 23835 if (!this._map.style) return; 23836 let attributions = []; 23837 if (this.options.customAttribution) { 23838 if (Array.isArray(this.options.customAttribution)) attributions = attributions.concat(this.options.customAttribution.map((attribution) => { 23839 if (typeof attribution !== "string") return ""; 23840 return attribution; 23841 })); 23842 else if (typeof this.options.customAttribution === "string") attributions.push(this.options.customAttribution); 23843 } 23844 if (this._map.style.stylesheet) { 23845 const stylesheet = this._map.style.stylesheet; 23846 this.styleOwner = stylesheet.owner; 23847 this.styleId = stylesheet.id; 23848 } 23849 const tileManagers = this._map.style.tileManagers; 23850 for (const id in tileManagers) { 23851 const tileManager = tileManagers[id]; 23852 if (tileManager.used || tileManager.usedForTerrain) { 23853 const source = tileManager.getSource(); 23854 if (source.attribution && !attributions.includes(source.attribution)) attributions.push(source.attribution); 23855 } 23856 } 23857 attributions = attributions.filter((e) => String(e).trim()); 23858 attributions.sort((a, b) => a.length - b.length); 23859 attributions = attributions.filter((attrib, i) => { 23860 for (let j = i + 1; j < attributions.length; j++) if (attributions[j].includes(attrib)) return false; 23861 return true; 23862 }); 23863 const attribHTML = attributions.join(" | "); 23864 if (attribHTML === this._attribHTML) return; 23865 this._attribHTML = attribHTML; 23866 if (attributions.length) { 23867 this._innerContainer.replaceChildren(DOM.sanitize(attribHTML)); 23868 this._container.classList.remove("maplibregl-attrib-empty"); 23869 } else this._container.classList.add("maplibregl-attrib-empty"); 23870 this._updateCompact(); 23871 this._editLink = null; 23872 } 23873}; 23874//#endregion 23875//#region src/ui/control/logo_control.ts 23876/** 23877* A `LogoControl` is a control that adds the watermark. 23878* 23879* @group Markers and Controls 23880* 23881* @example 23882* ```ts 23883* map.addControl(new LogoControl({compact: false})); 23884* ``` 23885**/ 23886var LogoControl = class { 23887 /** 23888 * @param options - the control's options 23889 */ 23890 constructor(options = {}) { 23891 this._updateCompact = () => { 23892 const containerChildren = this._container.children; 23893 if (containerChildren.length) { 23894 const anchor = containerChildren[0]; 23895 if (this._map.getCanvasContainer().offsetWidth <= 640 || this._compact) { 23896 if (this._compact !== false) anchor.classList.add("maplibregl-compact"); 23897 } else anchor.classList.remove("maplibregl-compact"); 23898 } 23899 }; 23900 this.options = options; 23901 } 23902 getDefaultPosition() { 23903 return "bottom-left"; 23904 } 23905 /** {@inheritDoc IControl.onAdd} */ 23906 onAdd(map) { 23907 this._map = map; 23908 this._compact = this.options?.compact; 23909 this._container = DOM.create("div", "maplibregl-ctrl"); 23910 const anchor = DOM.create("a", "maplibregl-ctrl-logo"); 23911 anchor.target = "_blank"; 23912 anchor.rel = "noopener nofollow"; 23913 anchor.href = "https://maplibre.org/"; 23914 anchor.setAttribute("aria-label", this._map._getUIString("LogoControl.Title")); 23915 anchor.setAttribute("rel", "noopener nofollow"); 23916 this._container.appendChild(anchor); 23917 this._container.style.display = "block"; 23918 this._map.on("resize", this._updateCompact); 23919 this._updateCompact(); 23920 return this._container; 23921 } 23922 /** {@inheritDoc IControl.onRemove} */ 23923 onRemove() { 23924 this._container.remove(); 23925 this._map.off("resize", this._updateCompact); 23926 this._map = void 0; 23927 this._compact = void 0; 23928 } 23929}; 23930//#endregion 23931//#region src/util/task_queue.ts 23932var TaskQueue = class { 23933 constructor() { 23934 this._queue = []; 23935 this._id = 0; 23936 this._cleared = false; 23937 this._currentlyRunning = false; 23938 } 23939 add(callback) { 23940 const id = ++this._id; 23941 this._queue.push({ 23942 callback, 23943 id, 23944 cancelled: false 23945 }); 23946 return id; 23947 } 23948 remove(id) { 23949 const running = this._currentlyRunning; 23950 const queue = running ? this._queue.concat(running) : this._queue; 23951 for (const task of queue) if (task.id === id) { 23952 task.cancelled = true; 23953 return; 23954 } 23955 } 23956 run(timeStamp = 0) { 23957 if (this._currentlyRunning) throw new Error("Attempting to run(), but is already running."); 23958 const queue = this._currentlyRunning = this._queue; 23959 this._queue = []; 23960 let failed = false; 23961 let firstError; 23962 for (const task of queue) { 23963 if (task.cancelled) continue; 23964 try { 23965 task.callback(timeStamp); 23966 } catch (error) { 23967 if (!failed) { 23968 failed = true; 23969 firstError = error; 23970 } 23971 } 23972 if (this._cleared) break; 23973 } 23974 this._cleared = false; 23975 this._currentlyRunning = false; 23976 if (failed) throw firstError; 23977 } 23978 clear() { 23979 if (this._currentlyRunning) this._cleared = true; 23980 this._queue = []; 23981 } 23982}; 23983//#endregion 23984//#region src/webgl/rtt_fingerprint.ts 23985/** 23986* What a render-to-texture tile's textures differ in from the state this frame would render them from, 23987* least to most severe: `zoom` (the same layers and source tiles, evaluated at another zoom), `sourceTiles` 23988* (other source tiles under the terrain tile), `visibleLayers` (a layer entered or left its zoom range) and 23989* `revision` (the source data changed, or the textures were rendered without this source). 23990* 23991* @internal 23992*/ 23993const RTT_DIFFERENCES = [ 23994 "none", 23995 "zoom", 23996 "sourceTiles", 23997 "visibleLayers", 23998 "revision" 23999]; 24000/** 24001* Immutable value describing the state a render-to-texture tile's textures were rendered from: the source 24002* tiles drawn into them, the source data revision, the map zoom (zoom-dependent style properties are 24003* evaluated then) and the ids of the layers visible at that zoom. 24004* 24005* @internal 24006*/ 24007var RTTFingerprint = class { 24008 constructor(coords, revision, zoom, visibleLayerIds) { 24009 this._tileKeys = coords.map((c) => c.key).sort().join(); 24010 this._revision = revision; 24011 this._zoom = zoom; 24012 this._visibleLayerIds = visibleLayerIds; 24013 } 24014 /** 24015 * The most severe difference between this fingerprint and the one the textures were rendered from, 24016 * `none` when they match. A missing fingerprint counts as a revision difference. 24017 */ 24018 difference(other) { 24019 if (this._revision !== other?._revision) return "revision"; 24020 if (this._visibleLayerIds !== other._visibleLayerIds) return "visibleLayers"; 24021 if (this._tileKeys !== other._tileKeys) return "sourceTiles"; 24022 return this._zoom === other._zoom ? "none" : "zoom"; 24023 } 24024}; 24025//#endregion 24026//#region src/webgl/render_to_texture.ts 24027/** 24028* lookup table which layers should rendered to texture 24029*/ 24030const LAYERS_TO_TEXTURES = { 24031 background: true, 24032 fill: true, 24033 line: true, 24034 raster: true, 24035 hillshade: true, 24036 "color-relief": true 24037}; 24038/** 24039* @internal 24040* Renders RTT-eligible layers into per-tile cached textures, then drapes 24041* them onto the terrain mesh. Slots live on each Tile so their lifetime 24042* tracks the tile itself; the underlying FBO+texture handles are recycled 24043* via the painter's RTT pool. 24044*/ 24045var RenderToTexture = class { 24046 constructor(painter, terrain) { 24047 this.needsFollowUpFrame = false; 24048 this.painter = painter; 24049 this.terrain = terrain; 24050 this.rttSize = terrain.tileManager.tileSize * terrain.qualityFactor; 24051 } 24052 getTexture(tile) { 24053 return tile.getRTT(this._stacks.length - 1).texture; 24054 } 24055 /** 24056 * Collects the frame's tiles, layers and source fingerprints and releases the textures that need 24057 * re-rendering. Textures that differ only by zoom are kept while the zoom is changing or the map is moving 24058 * and re-rendered one tile per frame once it has stopped, nearest to the camera first; textures with other 24059 * source tiles under them are re-rendered one per frame too, `needsFollowUpFrame` bringing in the rest. 24060 * Textures whose visible layer set changed (a layer entered or left its zoom range) are kept while the zoom 24061 * is changing and then all re-rendered in the same frame, since a tile-by-tile change would show; a source 24062 * data change re-renders immediately. 24063 */ 24064 prepareForRender(style, zoom) { 24065 const zoomChanged = zoom !== this._lastPrepareZoom; 24066 this._lastPrepareZoom = zoom; 24067 this._stacks = []; 24068 this._prevType = null; 24069 this._rttTiles = []; 24070 this._renderableTiles = this.terrain.tileManager.getRenderableTiles(); 24071 this._renderableLayerIds = style._order.filter((id) => !style._layers[id].isHidden(zoom)); 24072 const visibleLayerIds = this._renderableLayerIds.join(); 24073 const rttSourceIds = /* @__PURE__ */ new Set(); 24074 for (const layerId of this._renderableLayerIds) { 24075 const layer = style._layers[layerId]; 24076 const source = layer.source; 24077 if (source && LAYERS_TO_TEXTURES[layer.type]) rttSourceIds.add(source); 24078 } 24079 this._coordsAscending = {}; 24080 this._rttFingerprints = {}; 24081 for (const sourceId of rttSourceIds) { 24082 const tileManager = style.tileManagers[sourceId]; 24083 if (!tileManager) continue; 24084 this._coordsAscending[sourceId] = {}; 24085 const coordsAscending = this._coordsAscending[sourceId]; 24086 const source = tileManager.getSource(); 24087 const terrainTileRanges = source instanceof ImageSource ? source.terrainTileRanges : null; 24088 for (const tileID of tileManager.getVisibleCoordinates()) { 24089 const keys = this.terrain.tileManager.getTerrainCoords(tileID, terrainTileRanges); 24090 for (const key in keys) { 24091 coordsAscending[key] ||= []; 24092 coordsAscending[key].push(keys[key]); 24093 } 24094 } 24095 this._rttFingerprints[sourceId] = {}; 24096 const fingerprints = this._rttFingerprints[sourceId]; 24097 const revision = tileManager.getState().revision; 24098 for (const key in coordsAscending) fingerprints[key] = new RTTFingerprint(coordsAscending[key], revision, zoom, visibleLayerIds); 24099 } 24100 this.needsFollowUpFrame = false; 24101 const moving = zoomChanged || this.painter.options.moving; 24102 let staleTileReleased = false; 24103 for (const tile of this._renderableTiles) { 24104 const difference = this._textureDifference(tile); 24105 if (difference === "none") continue; 24106 if (difference === "zoom" && moving || difference === "visibleLayers" && zoomChanged) this.needsFollowUpFrame = true; 24107 else if (difference === "zoom" || difference === "sourceTiles") { 24108 if (staleTileReleased) this.needsFollowUpFrame = true; 24109 else tile.releaseRTT(this.painter); 24110 staleTileReleased = true; 24111 } else tile.releaseRTT(this.painter); 24112 } 24113 } 24114 /** 24115 * The most severe difference, over the sources rendered to texture, between the tile's cached textures 24116 * and what this frame would render into them. 24117 */ 24118 _textureDifference(tile) { 24119 let worst = "none"; 24120 for (const source in this._rttFingerprints) { 24121 const frameFingerprint = this._rttFingerprints[source][tile.tileID.key]; 24122 if (!frameFingerprint) continue; 24123 const difference = frameFingerprint.difference(tile.rttFingerprint[source]); 24124 if (RTT_DIFFERENCES.indexOf(difference) > RTT_DIFFERENCES.indexOf(worst)) worst = difference; 24125 } 24126 return worst; 24127 } 24128 /** 24129 * due that switching textures is relatively slow, the render 24130 * layer-by-layer context is not practicable. To bypass this problem 24131 * this lines of code stack all layers and later render all at once. 24132 * Because of the stylesheet possibility to mixing render-to-texture layers 24133 * and 'live'-layers (f.e. symbols) it is necessary to create more stacks. For example 24134 * a symbol-layer is in between of fill-layers. 24135 * @param layer - the layer to render 24136 * @param renderContext - shared state for the current render 24137 * @returns if true layer is rendered to texture, otherwise false 24138 */ 24139 renderLayer(layer, renderContext) { 24140 if (layer.isHidden(this.painter.transform.zoom)) return false; 24141 const type = layer.type; 24142 const painter = this.painter; 24143 const isLastLayer = this._renderableLayerIds[this._renderableLayerIds.length - 1] === layer.id; 24144 if (LAYERS_TO_TEXTURES[type]) { 24145 if (!this._prevType || !LAYERS_TO_TEXTURES[this._prevType]) this._stacks.push([]); 24146 this._prevType = type; 24147 this._stacks[this._stacks.length - 1].push(layer.id); 24148 if (!isLastLayer) return true; 24149 } 24150 if (LAYERS_TO_TEXTURES[this._prevType] || LAYERS_TO_TEXTURES[type] && isLastLayer) { 24151 this._prevType = type; 24152 const stack = this._stacks.length - 1, layers = this._stacks[stack] || []; 24153 renderContext.isRenderingToTexture = true; 24154 for (const tile of this._renderableTiles) { 24155 this._rttTiles.push(tile); 24156 if (tile.getRTT(stack)) continue; 24157 const obj = tile.acquireRTT(painter, stack, this.rttSize); 24158 painter.bindRTT(obj); 24159 painter.context.clear({ 24160 color: Color.transparent, 24161 stencil: 0 24162 }); 24163 painter.currentStencilSource = void 0; 24164 for (const layerId of layers) { 24165 const layer = painter.style._layers[layerId]; 24166 const coords = layer.source ? this._coordsAscending[layer.source][tile.tileID.key] : [tile.tileID]; 24167 painter.context.viewport.set([ 24168 0, 24169 0, 24170 this.rttSize, 24171 this.rttSize 24172 ]); 24173 painter.renderTileClippingMasks(layer, coords); 24174 painter.renderLayer(painter, painter.style.tileManagers[layer.source], layer, coords, renderContext); 24175 if (layer.source) tile.rttFingerprint[layer.source] = this._rttFingerprints[layer.source][tile.tileID.key]; 24176 } 24177 obj.texture.generateMipmap(); 24178 } 24179 renderContext.isRenderingToTexture = false; 24180 drawTerrain(this.painter, this.terrain, this._rttTiles, renderContext); 24181 this._rttTiles = []; 24182 return LAYERS_TO_TEXTURES[type]; 24183 } 24184 return false; 24185 } 24186}; 24187//#endregion 24188//#region src/ui/default_locale.ts 24189const defaultLocale = { 24190 "AttributionControl.ToggleAttribution": "Toggle attribution", 24191 "AttributionControl.MapFeedback": "Map feedback", 24192 "FullscreenControl.Enter": "Enter fullscreen", 24193 "FullscreenControl.Exit": "Exit fullscreen", 24194 "GeolocateControl.FindMyLocation": "Find my location", 24195 "GeolocateControl.LocationNotAvailable": "Location not available", 24196 "LogoControl.Title": "MapLibre logo", 24197 "Map.Title": "Map", 24198 "Marker.Title": "Map marker", 24199 "NavigationControl.ResetBearing": "Drag to rotate map, click to reset north", 24200 "NavigationControl.ZoomIn": "Zoom in", 24201 "NavigationControl.ZoomOut": "Zoom out", 24202 "Popup.Close": "Close popup", 24203 "ScaleControl.Feet": "ft", 24204 "ScaleControl.Meters": "m", 24205 "ScaleControl.Kilometers": "km", 24206 "ScaleControl.Miles": "mi", 24207 "ScaleControl.NauticalMiles": "nm", 24208 "GlobeControl.Enable": "Enable globe", 24209 "GlobeControl.Disable": "Disable globe", 24210 "TerrainControl.Enable": "Enable terrain", 24211 "TerrainControl.Disable": "Disable terrain", 24212 "CooperativeGesturesHandler.WindowsHelpText": "Use Ctrl + scroll to zoom the map", 24213 "CooperativeGesturesHandler.MacHelpText": "Use â + scroll to zoom the map", 24214 "CooperativeGesturesHandler.MobileHelpText": "Use two fingers to move the map" 24215}; 24216//#endregion 24217//#region src/ui/map.ts 24218const version$1 = version$2; 24219const defaultMinZoom = -2; 24220const defaultMaxZoom = 22; 24221const defaultMinPitch = 0; 24222const defaultMaxPitch = 60; 24223const defaultAnisotropicFilterPitch = 20; 24224const maxPitchThreshold = 180; 24225const defaultOptions$4 = { 24226 hash: false, 24227 interactive: true, 24228 bearingSnap: 7, 24229 zoomSnap: 0, 24230 attributionControl: defaultAttributionControlOptions, 24231 maplibreLogo: false, 24232 refreshExpiredTiles: true, 24233 canvasContextAttributes: { 24234 antialias: false, 24235 preserveDrawingBuffer: false, 24236 powerPreference: "high-performance", 24237 failIfMajorPerformanceCaveat: false, 24238 desynchronized: false, 24239 contextType: void 0 24240 }, 24241 scrollZoom: true, 24242 minZoom: defaultMinZoom, 24243 maxZoom: defaultMaxZoom, 24244 minPitch: defaultMinPitch, 24245 maxPitch: defaultMaxPitch, 24246 boxZoom: true, 24247 dragRotate: true, 24248 dragPan: true, 24249 keyboard: true, 24250 doubleClickZoom: true, 24251 touchZoomRotate: true, 24252 touchPitch: true, 24253 cooperativeGestures: false, 24254 trackResize: true, 24255 center: [0, 0], 24256 elevation: 0, 24257 zoom: 0, 24258 bearing: 0, 24259 pitch: 0, 24260 roll: 0, 24261 renderWorldCopies: true, 24262 maxTileCacheSize: null, 24263 maxTileCacheZoomLevels: config.MAX_TILE_CACHE_ZOOM_LEVELS, 24264 transformRequest: null, 24265 transformCameraUpdate: null, 24266 transformConstrain: null, 24267 fadeDuration: 300, 24268 crossSourceCollisions: true, 24269 clickTolerance: 3, 24270 localIdeographFontFamily: "sans-serif", 24271 pitchWithRotate: true, 24272 rollEnabled: false, 24273 rotateSpeed: .8, 24274 pitchSpeed: -.5, 24275 reduceMotion: void 0, 24276 validateStyle: true, 24277 /**Because GL MAX_TEXTURE_SIZE is usually at least 4096px. */ 24278 maxCanvasSize: [4096, 4096], 24279 cancelPendingTileRequestsWhileZooming: true, 24280 centerClampedToGround: true, 24281 terrainSkirtLength: "auto", 24282 zoomLevelsToOverscale: 4, 24283 anisotropicFilterPitch: defaultAnisotropicFilterPitch 24284}; 24285/** 24286* The `Map` object represents the map on your page. It exposes methods 24287* and properties that enable you to programmatically change the map, 24288* and fires events as users interact with it. 24289* 24290* You create a `Map` by specifying a `container` and other options, see {@link MapOptions} for the full list. 24291* Then MapLibre GL JS initializes the map on the page and returns your `Map` object. 24292* 24293* @group Main 24294* 24295* @example 24296* ```ts 24297* let map = new Map({ 24298* container: 'map', 24299* center: [-122.420679, 37.772537], 24300* zoom: 13, 24301* style: style_object, 24302* hash: true, 24303* transformRequest: (url, resourceType)=> { 24304* if(resourceType === 'Source' && url.startsWith('http://myHost')) { 24305* return { 24306* url: url.replace('http', 'https'), 24307* headers: { 'my-custom-header': true}, 24308* credentials: 'include' // Include cookies for cross-origin requests 24309* } 24310* } 24311* } 24312* }); 24313* ``` 24314* @see [Display a map](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-map/) 24315*/ 24316var Map$1 = class extends Evented { 24317 /** 24318 * @internal 24319 * The window that owns the map container element, for cross-window support. 24320 * Returns `typeof window` to include global constructors like ResizeObserver. 24321 */ 24322 get _ownerWindow() { 24323 return this._container?.ownerDocument?.defaultView || window; 24324 } 24325 constructor(options) { 24326 super(); 24327 this._idleTriggered = false; 24328 this._crossFadingFactor = 1; 24329 this._renderTaskQueue = new TaskQueue(); 24330 this._controls = []; 24331 this._mapId = uniqueId(); 24332 this._styleUrl = null; 24333 this._missingStyleImageResolver = null; 24334 this._lostContextStyle = { 24335 style: null, 24336 images: null 24337 }; 24338 this._contextLost = (event) => { 24339 event.preventDefault(); 24340 if (this._frameRequest) { 24341 this._frameRequest.abort(); 24342 this._frameRequest = null; 24343 } 24344 this.painter.destroy(); 24345 this._lostContextStyle = this._getStyleAndImages(); 24346 if (!this.style) { 24347 this.fire(new MapContextEvent("webglcontextlost", { originalEvent: event })); 24348 return; 24349 } 24350 for (const layer of Object.values(this.style._layers)) { 24351 if (layer.type === "custom") console.warn(`Custom layer with id '${layer.id}' cannot be restored after WebGL context loss. You will need to re-add it manually after context restoration.`); 24352 if (layer._listeners) for (const [event] of Object.entries(layer._listeners)) console.warn(`Custom layer with id '${layer.id}' had event listeners for event '${event}' which cannot be restored after WebGL context loss. You will need to re-add them manually after context restoration.`); 24353 } 24354 this.style.destroy(); 24355 this.style = null; 24356 this.fire(new MapContextEvent("webglcontextlost", { originalEvent: event })); 24357 }; 24358 this._contextRestored = (event) => { 24359 if (this._lostContextStyle.style) this.setStyle(this._lostContextStyle.style, { diff: false }); 24360 if (this._lostContextStyle.images && this.style) { 24361 this.style.imageManager.images = this._lostContextStyle.images; 24362 for (const id in this._lostContextStyle.images) { 24363 const image = this._lostContextStyle.images[id]; 24364 if (image.isWebGLImage) this.style.imageManager.updateImage(id, image, false); 24365 } 24366 } 24367 this._lostContextStyle = { 24368 style: null, 24369 images: null 24370 }; 24371 try { 24372 this._setupPainter(); 24373 } catch (error) { 24374 this.fire(new ErrorEvent(error)); 24375 return; 24376 } 24377 this.resize(); 24378 this._update(); 24379 this._resizeInternal(); 24380 this.fire(new MapContextEvent("webglcontextrestored", { originalEvent: event })); 24381 }; 24382 this._onMapScroll = (event) => { 24383 if (event.target !== this._container) return; 24384 this._container.scrollTop = 0; 24385 this._container.scrollLeft = 0; 24386 return false; 24387 }; 24388 this._onWindowOnline = () => { 24389 this._update(); 24390 }; 24391 const resolvedOptions = { 24392 ...defaultOptions$4, 24393 ...options, 24394 canvasContextAttributes: { 24395 ...defaultOptions$4.canvasContextAttributes, 24396 ...options.canvasContextAttributes 24397 } 24398 }; 24399 if (resolvedOptions.minZoom != null && resolvedOptions.maxZoom != null && resolvedOptions.minZoom > resolvedOptions.maxZoom) throw new Error("maxZoom must be greater than or equal to minZoom"); 24400 if (resolvedOptions.minPitch != null && resolvedOptions.maxPitch != null && resolvedOptions.minPitch > resolvedOptions.maxPitch) throw new Error("maxPitch must be greater than or equal to minPitch"); 24401 if (resolvedOptions.minPitch != null && resolvedOptions.minPitch < defaultMinPitch) throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); 24402 if (resolvedOptions.maxPitch != null && resolvedOptions.maxPitch > maxPitchThreshold) throw new Error(`maxPitch must be less than or equal to ${maxPitchThreshold}`); 24403 this._camera = new Camera({ 24404 minZoom: resolvedOptions.minZoom, 24405 maxZoom: resolvedOptions.maxZoom, 24406 minPitch: resolvedOptions.minPitch, 24407 maxPitch: resolvedOptions.maxPitch, 24408 bearingSnap: resolvedOptions.bearingSnap, 24409 zoomSnap: resolvedOptions.zoomSnap, 24410 renderWorldCopies: resolvedOptions.renderWorldCopies, 24411 centerClampedToGround: resolvedOptions.centerClampedToGround, 24412 terrain: this.terrain, 24413 transformConstrain: resolvedOptions.transformConstrain, 24414 requestRenderFrame: (callback) => this._requestRenderFrame(callback), 24415 cancelRenderFrame: (id) => this._cancelRenderFrame(id), 24416 transformCameraUpdate: resolvedOptions.transformCameraUpdate, 24417 stopHandlers: () => this._handlers?.stop(false) 24418 }); 24419 this._camera.setEventedParent(this); 24420 this._interactive = resolvedOptions.interactive; 24421 this._maxTileCacheSize = resolvedOptions.maxTileCacheSize; 24422 this._maxTileCacheZoomLevels = resolvedOptions.maxTileCacheZoomLevels; 24423 this._canvasContextAttributes = { ...resolvedOptions.canvasContextAttributes }; 24424 this._trackResize = resolvedOptions.trackResize === true; 24425 this._terrainSkirtLength = resolvedOptions.terrainSkirtLength; 24426 this._refreshExpiredTiles = resolvedOptions.refreshExpiredTiles === true; 24427 this._fadeDuration = resolvedOptions.fadeDuration; 24428 this._crossSourceCollisions = resolvedOptions.crossSourceCollisions === true; 24429 this._collectResourceTiming = resolvedOptions.collectResourceTiming === true; 24430 this._locale = { 24431 ...defaultLocale, 24432 ...resolvedOptions.locale 24433 }; 24434 this._clickTolerance = resolvedOptions.clickTolerance; 24435 this._overridePixelRatio = resolvedOptions.pixelRatio; 24436 this._maxCanvasSize = resolvedOptions.maxCanvasSize; 24437 this._zoomLevelsToOverscale = resolvedOptions.zoomLevelsToOverscale; 24438 this.cancelPendingTileRequestsWhileZooming = resolvedOptions.cancelPendingTileRequestsWhileZooming === true; 24439 this.setAnisotropicFilterPitch(resolvedOptions.anisotropicFilterPitch); 24440 if (resolvedOptions.reduceMotion !== void 0) browser.prefersReducedMotion = resolvedOptions.reduceMotion; 24441 this._requestManager = new RequestManager(resolvedOptions.transformRequest); 24442 this._container = this._resolveContainer(resolvedOptions.container); 24443 if (resolvedOptions.maxBounds) this.setMaxBounds(resolvedOptions.maxBounds); 24444 this._setupContainer(); 24445 try { 24446 this._setupPainter(); 24447 } catch (error) { 24448 this._cleanupContainer(); 24449 throw error; 24450 } 24451 this._imageQueueHandle = ImageRequest.addThrottleControl(() => this.isMoving()); 24452 this.on("move", () => this._update(false)); 24453 this.on("moveend", () => this._update(false)); 24454 this.on("zoom", () => this._update(true)); 24455 this.on("terrain", () => { 24456 this.painter.markTerrainDepthDirty(); 24457 this._update(true); 24458 }); 24459 this.once("idle", () => this._idleTriggered = true); 24460 this._handlers = new HandlerManager(this, this._camera, resolvedOptions); 24461 if (typeof window !== "undefined") { 24462 this._ownerWindow.addEventListener("online", this._onWindowOnline, false); 24463 this._setupResizeObserver(); 24464 } 24465 const hashName = typeof resolvedOptions.hash === "string" && resolvedOptions.hash || void 0; 24466 this._hash = resolvedOptions.hash ? new Hash(hashName).addTo(this) : void 0; 24467 if (!this._hash?._onHashChange()) { 24468 this.jumpTo({ 24469 center: resolvedOptions.center, 24470 elevation: resolvedOptions.elevation, 24471 zoom: resolvedOptions.zoom, 24472 bearing: resolvedOptions.bearing, 24473 pitch: resolvedOptions.pitch, 24474 roll: resolvedOptions.roll 24475 }); 24476 if (resolvedOptions.bounds) { 24477 this.resize(); 24478 this.fitBounds(resolvedOptions.bounds, extend({}, resolvedOptions.fitBoundsOptions, { duration: 0 })); 24479 } 24480 } 24481 const shouldConstrainUsingMercatorTransform = typeof resolvedOptions.style === "string" || !(resolvedOptions.style?.projection?.type === "globe"); 24482 this.resize(null, shouldConstrainUsingMercatorTransform); 24483 this._localIdeographFontFamily = resolvedOptions.localIdeographFontFamily; 24484 this._validateStyle = resolvedOptions.validateStyle; 24485 if (resolvedOptions.style) this.setStyle(resolvedOptions.style, { localIdeographFontFamily: resolvedOptions.localIdeographFontFamily }); 24486 if (resolvedOptions.attributionControl) this.addControl(new AttributionControl(typeof resolvedOptions.attributionControl === "boolean" ? void 0 : resolvedOptions.attributionControl)); 24487 if (resolvedOptions.maplibreLogo) this.addControl(new LogoControl(), resolvedOptions.logoPosition); 24488 this.on("style.load", () => { 24489 if (!shouldConstrainUsingMercatorTransform) this._resizeTransform(); 24490 if (this._camera.transform.unmodified) { 24491 const coercedOptions = pick(this.style.stylesheet, [ 24492 "center", 24493 "zoom", 24494 "bearing", 24495 "pitch", 24496 "roll" 24497 ]); 24498 this.jumpTo(coercedOptions); 24499 } 24500 }); 24501 this.on("data", (event) => { 24502 this._update(event.dataType === "style"); 24503 this.fire(event.dataType === "style" ? new MapStyleDataEvent("styledata", event) : new MapSourceDataEvent("sourcedata", event)); 24504 }); 24505 this.on("dataloading", (event) => { 24506 this.fire(event.dataType === "style" ? new MapStyleDataEvent("styledataloading", event) : new MapSourceDataEvent("sourcedataloading", event)); 24507 }); 24508 this.on("dataabort", (event) => { 24509 this.fire(new MapSourceDataEvent("sourcedataabort", event)); 24510 }); 24511 } 24512 /** 24513 * @internal 24514 * Returns a unique number for this map instance which is used for the MapLoadEvent 24515 * to make sure we only fire one event per instantiated map object. 24516 * @returns the uniq map ID 24517 */ 24518 _getMapId() { 24519 return this._mapId; 24520 } 24521 /** 24522 * Sets a global state property that can be retrieved with the [`global-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#global-state). 24523 * If the value is null, it resets the property to its default value defined in the [`state` style property](https://maplibre.org/maplibre-style-spec/root/#state). 24524 * 24525 * @param propertyName - The name of the state property to set. 24526 * @param value - The value of the state property to set. 24527 */ 24528 setGlobalStateProperty(propertyName, value) { 24529 this.style.setGlobalStateProperty(propertyName, value); 24530 return this._update(true); 24531 } 24532 /** 24533 * Returns the global map state 24534 * 24535 * @returns The map state object. 24536 */ 24537 getGlobalState() { 24538 return this.style.getGlobalState(); 24539 } 24540 /** 24541 * Adds an {@link IControl} to the map, calling `control.onAdd(this)`. 24542 * 24543 * An {@link ErrorEvent} will be fired if the control is invalid. 24544 * 24545 * @param control - The {@link IControl} to add. 24546 * @param position - position on the map to which the control will be added. 24547 * Valid values are `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`. Defaults to `'top-right'`. 24548 * @example 24549 * Add zoom and rotation controls to the map. 24550 * ```ts 24551 * map.addControl(new NavigationControl()); 24552 * ``` 24553 * @see [Display map navigation controls](https://maplibre.org/maplibre-gl-js/docs/examples/display-map-navigation-controls/) 24554 */ 24555 addControl(control, position) { 24556 if (position === void 0) { 24557 if (control.getDefaultPosition) position = control.getDefaultPosition(); 24558 else position = "top-right"; 24559 } 24560 if (!control?.onAdd) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods."))); 24561 const controlElement = control.onAdd(this); 24562 this._controls.push(control); 24563 const positionContainer = this._controlPositions[position]; 24564 if (position.includes("bottom")) positionContainer.insertBefore(controlElement, positionContainer.firstChild); 24565 else positionContainer.appendChild(controlElement); 24566 return this; 24567 } 24568 /** 24569 * Removes the control from the map. 24570 * 24571 * An {@link ErrorEvent} will be fired if the control is invalid. 24572 * 24573 * @param control - The {@link IControl} to remove. 24574 * @example 24575 * ```ts 24576 * // Define a new navigation control. 24577 * let navigation = new NavigationControl(); 24578 * // Add zoom and rotation controls to the map. 24579 * map.addControl(navigation); 24580 * // Remove zoom and rotation controls from the map. 24581 * map.removeControl(navigation); 24582 * ``` 24583 */ 24584 removeControl(control) { 24585 if (!control?.onRemove) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods."))); 24586 const ci = this._controls.indexOf(control); 24587 if (ci > -1) this._controls.splice(ci, 1); 24588 control.onRemove(this); 24589 return this; 24590 } 24591 /** 24592 * Checks if a control exists on the map. 24593 * 24594 * @param control - The {@link IControl} to check. 24595 * @returns true if map contains control. 24596 * @example 24597 * ```ts 24598 * // Define a new navigation control. 24599 * let navigation = new NavigationControl(); 24600 * // Add zoom and rotation controls to the map. 24601 * map.addControl(navigation); 24602 * // Check that the navigation control exists on the map. 24603 * map.hasControl(navigation); 24604 * ``` 24605 */ 24606 hasControl(control) { 24607 return this._controls.includes(control); 24608 } 24609 /** 24610 * Returns an array of `OverscaledTileID` objects that cover the current viewport for a given tile size. 24611 * This method is useful for determining which tiles are visible in the current viewport. 24612 * 24613 * @param options - Options for calculating the covering tiles. 24614 * @returns An array of `OverscaledTileID` objects. 24615 * @example 24616 * ```ts 24617 * // Get the tiles to cover the view for a 512x512px tile source 24618 * const tiles = map.coveringTiles({tileSize: 512}); 24619 * ``` 24620 */ 24621 coveringTiles(options) { 24622 return coveringTiles(this._camera.transform, options); 24623 } 24624 /** 24625 * Sets the callback used to defer camera updates or apply arbitrary constraints. 24626 * If specified, this Camera instance can be used as a stateless component in React etc. 24627 */ 24628 setTransformCameraUpdate(value) { 24629 this._camera.transformCameraUpdate = value; 24630 } 24631 /** 24632 * Returns the map's geographical centerpoint. 24633 * 24634 * @returns The map's geographical centerpoint. 24635 * @example 24636 * Return a LngLat object such as `{lng: 0, lat: 0}` 24637 * ```ts 24638 * let center = map.getCenter(); 24639 * // access longitude and latitude values directly 24640 * let {lng, lat} = map.getCenter(); 24641 * ``` 24642 */ 24643 getCenter() { 24644 return new LngLat(this._camera.transform.center.lng, this._camera.transform.center.lat); 24645 } 24646 /** 24647 * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`. 24648 * 24649 * Triggers the following events: `movestart` and `moveend`. 24650 * 24651 * @param center - The centerpoint to set. 24652 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24653 * @example 24654 * ```ts 24655 * map.setCenter([-74, 38]); 24656 * ``` 24657 */ 24658 setCenter(center, eventData) { 24659 this._camera.setCenter(center, eventData); 24660 return this; 24661 } 24662 /** 24663 * Returns the elevation of the map's center point. 24664 * 24665 * @returns The elevation of the map's center point, in meters above sea level. 24666 */ 24667 getCenterElevation() { 24668 return this._camera.transform.elevation; 24669 } 24670 /** 24671 * Sets the elevation of the map's center point, in meters above sea level. Equivalent to `jumpTo({elevation: elevation})`. 24672 * 24673 * Triggers the following events: `movestart` and `moveend`. 24674 * 24675 * @param elevation - The elevation to set, in meters above sea level. 24676 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24677 */ 24678 setCenterElevation(elevation, eventData) { 24679 this._camera.setCenterElevation(elevation, eventData); 24680 return this; 24681 } 24682 /** 24683 * Sets the value of `centerClampedToGround`. 24684 * 24685 * If true, the elevation of the center point will automatically be set to the terrain elevation 24686 * (or zero if terrain is not enabled). If false, the elevation of the center point will default 24687 * to sea level and will not automatically update. Defaults to true. Needs to be set to false to 24688 * keep the camera above ground when pitch \> 90 degrees. 24689 */ 24690 setCenterClampedToGround(centerClampedToGround) { 24691 this._camera.setCenterClampedToGround(centerClampedToGround); 24692 } 24693 /** 24694 * Pans the map by the specified offset. 24695 * 24696 * Triggers the following events: `movestart` and `moveend`. 24697 * 24698 * @param offset - `x` and `y` coordinates by which to pan the map. 24699 * @param options - Options object 24700 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24701 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js/docs/examples/navigate-the-map-with-game-like-controls/) 24702 */ 24703 panBy(offset, options, eventData) { 24704 this._camera.panBy(offset, options, eventData); 24705 return this; 24706 } 24707 /** 24708 * Pans the map to the specified location with an animated transition. 24709 * 24710 * Triggers the following events: `movestart` and `moveend`. 24711 * 24712 * @param lnglat - The location to pan the map to. 24713 * @param options - Options describing the destination and animation of the transition. 24714 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24715 * @example 24716 * ```ts 24717 * map.panTo([-74, 38]); 24718 * // Specify that the panTo animation should last 5000 milliseconds. 24719 * map.panTo([-74, 38], {duration: 5000}); 24720 * ``` 24721 * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js/docs/examples/update-a-feature-in-realtime/) 24722 */ 24723 panTo(lnglat, options, eventData) { 24724 this._camera.panTo(lnglat, options, eventData); 24725 return this; 24726 } 24727 /** 24728 * Returns the map's current zoom level. 24729 * 24730 * @returns The map's current zoom level. 24731 * @example 24732 * ```ts 24733 * map.getZoom(); 24734 * ``` 24735 */ 24736 getZoom() { 24737 return this._camera.transform.zoom; 24738 } 24739 /** 24740 * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`. 24741 * 24742 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 24743 * 24744 * @param zoom - The zoom level to set (0-20). 24745 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24746 * @example 24747 * Zoom to the zoom level 5 without an animated transition 24748 * ```ts 24749 * map.setZoom(5); 24750 * ``` 24751 */ 24752 setZoom(zoom, eventData) { 24753 this._camera.setZoom(zoom, eventData); 24754 return this; 24755 } 24756 /** 24757 * Zooms the map to the specified zoom level, with an animated transition. 24758 * 24759 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 24760 * 24761 * @param zoom - The zoom level to transition to. 24762 * @param options - Options object 24763 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24764 * @example 24765 * ```ts 24766 * // Zoom to the zoom level 5 without an animated transition 24767 * map.zoomTo(5); 24768 * // Zoom to the zoom level 8 with an animated transition 24769 * map.zoomTo(8, { 24770 * duration: 2000, 24771 * offset: [100, 50] 24772 * }); 24773 * ``` 24774 */ 24775 zoomTo(zoom, options, eventData) { 24776 this._camera.zoomTo(zoom, options, eventData); 24777 return this; 24778 } 24779 /** 24780 * Incrementally increases the map's zoom level by 1, first snapping to the nearest `zoomSnap` increment. 24781 * 24782 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 24783 * 24784 * @param options - Options object 24785 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24786 * @example 24787 * Zoom the map in one level with a custom animation duration 24788 * ```ts 24789 * map.zoomIn({duration: 1000}); 24790 * ``` 24791 */ 24792 zoomIn(options, eventData) { 24793 this._camera.zoomIn(options, eventData); 24794 return this; 24795 } 24796 /** 24797 * Decreases the map's zoom level by 1, first snapping to the nearest `zoomSnap` increment. 24798 * 24799 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 24800 * 24801 * @param options - Options object 24802 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24803 * @example 24804 * Zoom the map out one level with a custom animation offset 24805 * ```ts 24806 * map.zoomOut({offset: [80, 60]}); 24807 * ``` 24808 */ 24809 zoomOut(options, eventData) { 24810 this._camera.zoomOut(options, eventData); 24811 return this; 24812 } 24813 /** 24814 * Returns the map's current vertical field of view, in degrees. 24815 * 24816 * @returns The map's current vertical field of view. 24817 * @defaultValue 36.87 24818 * @example 24819 * ```ts 24820 * const verticalFieldOfView = map.getVerticalFieldOfView(); 24821 * ``` 24822 */ 24823 getVerticalFieldOfView() { 24824 return this._camera.transform.fov; 24825 } 24826 /** 24827 * Sets the map's vertical field of view, in degrees. 24828 * 24829 * Triggers the following events: `movestart`, `move`, and `moveend`. 24830 * 24831 * @param fov - The vertical field of view to set, in degrees (0-180). 24832 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24833 * @defaultValue 36.87 24834 * @example 24835 * Change vertical field of view to 30 degrees 24836 * ```ts 24837 * map.setVerticalFieldOfView(30); 24838 * ``` 24839 */ 24840 setVerticalFieldOfView(fov, eventData) { 24841 this._camera.setVerticalFieldOfView(fov, eventData); 24842 return this; 24843 } 24844 /** 24845 * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing 24846 * of 90° orients the map so that east is up. 24847 * 24848 * @returns The map's current bearing. 24849 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js/docs/examples/navigate-the-map-with-game-like-controls/) 24850 */ 24851 getBearing() { 24852 return this._camera.transform.bearing; 24853 } 24854 /** 24855 * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing 24856 * of 90° orients the map so that east is up. 24857 * 24858 * Equivalent to `jumpTo({bearing: bearing})`. 24859 * 24860 * Triggers the following events: `movestart`, `moveend`, and `rotate`. 24861 * 24862 * @param bearing - The desired bearing. 24863 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24864 * @example 24865 * Rotate the map to 90 degrees 24866 * ```ts 24867 * map.setBearing(90); 24868 * ``` 24869 */ 24870 setBearing(bearing, eventData) { 24871 this._camera.setBearing(bearing, eventData); 24872 return this; 24873 } 24874 /** 24875 * Returns the map's current zoom snap level. 24876 * 24877 * @returns The map's current zoom snap level. 24878 */ 24879 getZoomSnap() { 24880 return this._camera.getZoomSnap(); 24881 } 24882 /** 24883 * Sets the map's zoom snap level. 24884 * 24885 * @param snap - The zoom snap level to set. 24886 */ 24887 setZoomSnap(snap) { 24888 this._camera.setZoomSnap(snap); 24889 return this; 24890 } 24891 /** 24892 * Returns the current padding applied around the map viewport. 24893 * 24894 * @returns The current padding around the map viewport. 24895 */ 24896 getPadding() { 24897 return this._camera.transform.padding; 24898 } 24899 /** 24900 * Sets the padding in pixels around the viewport. 24901 * 24902 * Equivalent to `jumpTo({padding: padding})`. 24903 * 24904 * Triggers the following events: `movestart` and `moveend`. 24905 * 24906 * @param padding - The desired padding. 24907 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24908 * @example 24909 * Sets a left padding of 300px, and a top padding of 50px 24910 * ```ts 24911 * map.setPadding({ left: 300, top: 50 }); 24912 * ``` 24913 */ 24914 setPadding(padding, eventData) { 24915 this._camera.setPadding(padding, eventData); 24916 return this; 24917 } 24918 /** 24919 * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction 24920 * that is "up"; for example, a bearing of 90° orients the map so that east is up. 24921 * 24922 * Triggers the following events: `movestart`, `moveend`, and `rotate`. 24923 * 24924 * @param bearing - The desired bearing. 24925 * @param options - Options object 24926 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24927 */ 24928 rotateTo(bearing, options, eventData) { 24929 this._camera.rotateTo(bearing, options, eventData); 24930 return this; 24931 } 24932 /** 24933 * Rotates the map so that north is up (0° bearing), with an animated transition. 24934 * 24935 * Triggers the following events: `movestart`, `moveend`, and `rotate`. 24936 * 24937 * @param options - Options object 24938 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24939 */ 24940 resetNorth(options, eventData) { 24941 this._camera.resetNorth(options, eventData); 24942 return this; 24943 } 24944 /** 24945 * Rotates and pitches the map so that north is up (0° bearing) and pitch and roll are 0°, with an animated transition. 24946 * 24947 * Triggers the following events: `movestart`, `move`, `moveend`, `pitchstart`, `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. 24948 * 24949 * @param options - Options object 24950 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24951 */ 24952 resetNorthPitch(options, eventData) { 24953 this._camera.resetNorthPitch(options, eventData); 24954 return this; 24955 } 24956 /** 24957 * Snaps the map so that north is up (0° bearing), if the current bearing is close enough to it (i.e. within the 24958 * `bearingSnap` threshold). 24959 * 24960 * Triggers the following events: `movestart`, `moveend`, and `rotate`. 24961 * 24962 * @param options - Options object 24963 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24964 */ 24965 snapToNorth(options, eventData) { 24966 this._camera.snapToNorth(options, eventData); 24967 return this; 24968 } 24969 /** 24970 * Returns the map's current pitch (tilt). 24971 * 24972 * @returns The map's current pitch, measured in degrees away from the plane of the screen. 24973 */ 24974 getPitch() { 24975 return this._camera.transform.pitch; 24976 } 24977 /** 24978 * Sets the map's pitch (tilt). Equivalent to `jumpTo({pitch: pitch})`. 24979 * 24980 * Triggers the following events: `movestart`, `moveend`, `pitchstart`, and `pitchend`. 24981 * 24982 * @param pitch - The pitch to set, measured in degrees away from the plane of the screen (0-60). 24983 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 24984 */ 24985 setPitch(pitch, eventData) { 24986 this._camera.setPitch(pitch, eventData); 24987 return this; 24988 } 24989 /** 24990 * Returns the map's current roll angle. 24991 * 24992 * @returns The map's current roll, measured in degrees about the camera boresight. 24993 */ 24994 getRoll() { 24995 return this._camera.transform.roll; 24996 } 24997 /** 24998 * Sets the map's roll angle. Equivalent to `jumpTo({roll: roll})`. 24999 * 25000 * Triggers the following events: `movestart`, `moveend`, `rollstart`, and `rollend`. 25001 * 25002 * @param roll - The roll to set, measured in degrees about the camera boresight 25003 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25004 */ 25005 setRoll(roll, eventData) { 25006 this._camera.setRoll(roll, eventData); 25007 return this; 25008 } 25009 /** 25010 * @param bounds - Calculate the center for these bounds in the viewport and use 25011 * the highest zoom level up to and including {@link Map.getMaxZoom} that fits 25012 * in the viewport. LngLatBounds represent a box that is always axis-aligned with bearing 0. 25013 * Bounds will be taken in `[sw, ne]` order. Southwest point will always be to the left of the northeast point. 25014 * @param options - Options object 25015 * @returns If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`. 25016 * If map is unable to fit, method will warn and return undefined. 25017 * @example 25018 * ```ts 25019 * let bbox = [[-79, 43], [-73, 45]]; 25020 * let newCameraTransform = map.cameraForBounds(bbox, { 25021 * padding: {top: 10, bottom:25, left: 15, right: 5} 25022 * }); 25023 * ``` 25024 */ 25025 cameraForBounds(bounds, options) { 25026 return this._camera.cameraForBounds(bounds, options); 25027 } 25028 /** 25029 * Pans and zooms the map to contain its visible area within the specified geographical bounds. 25030 * This function will also reset the map's bearing to 0 if bearing is nonzero. 25031 * 25032 * Triggers the following events: `movestart` and `moveend`. 25033 * 25034 * @param bounds - Center these bounds in the viewport and use the highest 25035 * zoom level up to and including {@link Map.getMaxZoom} that fits them in the viewport. 25036 * Bounds will be taken in `[sw, ne]` order. Southwest point will always be to the left of the northeast point. 25037 * @param options - Options supports all properties from {@link AnimationOptions} and {@link CameraOptions} in addition to the fields below. 25038 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25039 * @example 25040 * ```ts 25041 * let bbox = [[-79, 43], [-73, 45]]; 25042 * map.fitBounds(bbox, { 25043 * padding: {top: 10, bottom:25, left: 15, right: 5} 25044 * }); 25045 * ``` 25046 * @see [Fit a map to a bounding box](https://maplibre.org/maplibre-gl-js/docs/examples/fit-a-map-to-a-bounding-box/) 25047 */ 25048 fitBounds(bounds, options, eventData) { 25049 this._camera.fitBounds(bounds, options, eventData); 25050 return this; 25051 } 25052 /** 25053 * Pans, rotates and zooms the map to to fit the box made by points p0 and p1 25054 * once the map is rotated to the specified bearing. To zoom without rotating, 25055 * pass in the current map bearing. 25056 * 25057 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend` and `rotate`. 25058 * 25059 * @param p0 - First point on screen, in pixel coordinates 25060 * @param p1 - Second point on screen, in pixel coordinates 25061 * @param bearing - Desired map bearing at end of animation, in degrees 25062 * @param options - Options object 25063 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25064 * @example 25065 * ```ts 25066 * let p0 = [220, 400]; 25067 * let p1 = [500, 900]; 25068 * map.fitScreenCoordinates(p0, p1, map.getBearing(), { 25069 * padding: {top: 10, bottom:25, left: 15, right: 5} 25070 * }); 25071 * ``` 25072 * @see Used by {@link BoxZoomHandler} 25073 */ 25074 fitScreenCoordinates(p0, p1, bearing, options, eventData) { 25075 this._camera.fitScreenCoordinates(p0, p1, bearing, options, eventData); 25076 return this; 25077 } 25078 /** 25079 * Changes any combination of center, zoom, bearing, pitch, and roll, without 25080 * an animated transition. The map will retain its current values for any 25081 * details not specified in `options`. 25082 * 25083 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, 25084 * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend` and `rotate`. 25085 * 25086 * @param options - Options object 25087 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25088 * @example 25089 * ```ts 25090 * // jump to coordinates at current zoom 25091 * map.jumpTo({center: [0, 0]}); 25092 * // jump with zoom, pitch, and bearing options 25093 * map.jumpTo({ 25094 * center: [0, 0], 25095 * zoom: 8, 25096 * pitch: 45, 25097 * bearing: 90 25098 * }); 25099 * ``` 25100 * @see [Jump to a series of locations](https://maplibre.org/maplibre-gl-js/docs/examples/jump-to-a-series-of-locations/) 25101 * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js/docs/examples/update-a-feature-in-realtime/) 25102 */ 25103 jumpTo(options, eventData) { 25104 this._camera.jumpTo(options, eventData); 25105 return this; 25106 } 25107 /** 25108 * Calculates constrained camera options that place a geographic anchor at a screen point 25109 * without changing the map, using the same logic as MapLibre's interaction handlers. 25110 * 25111 * @param options - Anchor and optional zoom. 25112 * @returns Camera options that can be passed to {@link Map.jumpTo}. 25113 * @example 25114 * ```ts 25115 * const cameraOptions = map.calculateAnchoredCameraOptions({ 25116 * anchorLocation: map.unproject(pointerDownPosition), 25117 * anchorScreenPoint: currentPointerPosition, 25118 * zoom: map.getZoom() + 1, 25119 * }); 25120 * map.jumpTo(cameraOptions); 25121 * ``` 25122 */ 25123 calculateAnchoredCameraOptions(options) { 25124 return this._camera.calculateAnchoredCameraOptions(options); 25125 } 25126 /** 25127 * Given a camera position and rotation, calculates zoom and center point and returns them as {@link CameraOptions}. 25128 * @param cameraLngLat - The lng, lat of the camera to look from 25129 * @param cameraAlt - The altitude of the camera to look from, in meters above sea level 25130 * @param bearing - Bearing of the camera, in degrees 25131 * @param pitch - Pitch of the camera, in degrees 25132 * @param roll - Roll of the camera, in degrees 25133 * @returns the calculated camera options 25134 * @example 25135 * ```ts 25136 * // Calculate options to look from camera position(1°, 0°, 1000m) with bearing = 90°, pitch = 30°, and roll = 45° 25137 * const cameraLngLat = new LngLat(1, 0); 25138 * const cameraAltitude = 1000; 25139 * const bearing = 90; 25140 * const pitch = 30; 25141 * const roll = 45; 25142 * const cameraOptions = map.calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat, cameraAltitude, bearing, pitch, roll); 25143 * // Apply calculated options 25144 * map.jumpTo(cameraOptions); 25145 * ``` 25146 */ 25147 calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat, cameraAlt, bearing, pitch, roll) { 25148 return this._camera.calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat, cameraAlt, bearing, pitch, roll); 25149 } 25150 /** 25151 * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, `roll`, and `padding` with an animated transition 25152 * between old and new values. The map will retain its current values for any 25153 * details not specified in `options`. 25154 * 25155 * !!! note "Reduced Motion" 25156 * The transition will happen instantly if the user has enabled 25157 * the `reduced motion` accessibility feature enabled in their operating system, 25158 * unless `options` includes `essential: true`. 25159 * 25160 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, 25161 * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. 25162 * 25163 * @param options - Options describing the destination and animation of the transition. 25164 * Accepts {@link CameraOptions} and {@link AnimationOptions}. 25165 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25166 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js/docs/examples/navigate-the-map-with-game-like-controls/) 25167 */ 25168 easeTo(options, eventData) { 25169 this._camera.easeTo(options, eventData); 25170 return this; 25171 } 25172 /** 25173 * Changes any combination of center, zoom, bearing, pitch, and roll, animating the transition along a curve that 25174 * evokes flight. The animation seamlessly incorporates zooming and panning to help 25175 * the user maintain her bearings even after traversing a great distance. 25176 * 25177 * !!! note "Reduced Motion" 25178 * The animation will be skipped, and this will behave equivalently to `jumpTo` 25179 * if the user has the `reduced motion` accessibility feature enabled in their operating system, 25180 * unless 'options' includes `essential: true`. 25181 * 25182 * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, 25183 * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. 25184 * 25185 * @param options - Options describing the destination and animation of the transition. 25186 * Accepts {@link CameraOptions}, {@link AnimationOptions}, 25187 * and the following additional options. 25188 * @param eventData - Additional properties to be added to event objects of events triggered by this method. 25189 * @example 25190 * ```ts 25191 * // fly with default options to null island 25192 * map.flyTo({center: [0, 0], zoom: 9}); 25193 * // using flyTo options 25194 * map.flyTo({ 25195 * center: [0, 0], 25196 * zoom: 9, 25197 * speed: 0.2, 25198 * curve: 1, 25199 * easing(t) { 25200 * return t; 25201 * } 25202 * }); 25203 * ``` 25204 * @see [Fly to a location](https://maplibre.org/maplibre-gl-js/docs/examples/fly-to-a-location/) 25205 * @see [Slowly fly to a location](https://maplibre.org/maplibre-gl-js/docs/examples/slowly-fly-to-a-location/) 25206 * @see [Fly to a location based on scroll position](https://maplibre.org/maplibre-gl-js/docs/examples/fly-to-a-location-based-on-scroll-position/) 25207 */ 25208 flyTo(options, eventData) { 25209 this._camera.flyTo(options, eventData); 25210 return this; 25211 } 25212 /** 25213 * Stops any animated transition underway. 25214 */ 25215 stop() { 25216 this._camera.stop(); 25217 return this; 25218 } 25219 /** 25220 * Gets the elevation at a given location, in meters above sea level. 25221 * Returns null if terrain is not enabled. 25222 * If terrain is enabled with some exaggeration value, the value returned here will be reflective of (multiplied by) that exaggeration value. 25223 * This method should be used for proper positioning of custom 3d objects, as explained [here](https://maplibre.org/maplibre-gl-js/docs/examples/adding-3d-models-using-threejs-on-terrain/) 25224 * @param lngLatLike - `[x, y]` or LngLat coordinates of the location 25225 * @returns elevation in meters 25226 */ 25227 queryTerrainElevation(lngLatLike) { 25228 if (!this.terrain) return null; 25229 return this.terrain.getElevationForLngLat(LngLat.convert(lngLatLike), this._camera.transform); 25230 } 25231 /** 25232 * Returns the value of `centerClampedToGround`. 25233 * 25234 * If true, the elevation of the center point will automatically be set to the terrain elevation 25235 * (or zero if terrain is not enabled). If false, the elevation of the center point will default 25236 * to sea level and will not automatically update. Defaults to true. Needs to be set to false to 25237 * keep the camera above ground when pitch \> 90 degrees. 25238 */ 25239 getCenterClampedToGround() { 25240 return this._camera.getCenterClampedToGround(); 25241 } 25242 /** 25243 * Given a camera 'from' position and a position to look at (`to`), calculates zoom and camera rotation and returns them as {@link CameraOptions}. 25244 * Under `globe` and `vertical-perspective` the calculation follows the sphere while the map renders as a globe, keeping the point looked at on the sea-level sphere; `altitudeTo` only becomes the center elevation. 25245 * @param from - The camera to look from 25246 * @param altitudeFrom - The altitude of the camera to look from 25247 * @param to - The center to look at 25248 * @param altitudeTo - Optional altitude of the center to look at. If none given the ground height will be used. 25249 * @returns the calculated camera options 25250 * @example 25251 * ```ts 25252 * // Calculate options to look from (1°, 0°, 1000m) to (1°, 1°, 0m) 25253 * const cameraLngLat = new LngLat(1, 0); 25254 * const cameraAltitude = 1000; 25255 * const targetLngLat = new LngLat(1, 1); 25256 * const targetAltitude = 0; 25257 * const cameraOptions = map.calculateCameraOptionsFromTo(cameraLngLat, cameraAltitude, targetLngLat, targetAltitude); 25258 * // Apply calculated options 25259 * map.jumpTo(cameraOptions); 25260 * ``` 25261 */ 25262 calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo) { 25263 if (altitudeTo == null && this.terrain) altitudeTo = this.terrain.getElevationForLngLat(to, this._camera.transform); 25264 return this._camera.transform.calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo ?? 0); 25265 } 25266 /** 25267 * Resizes the map according to the dimensions of its 25268 * `container` element. 25269 * 25270 * Checks if the map container size changed and updates the map if it has changed. 25271 * With the default `trackResize: true`, container size changes are picked up automatically, 25272 * including a container that becomes visible after being hidden with CSS. Call this method 25273 * explicitly when `trackResize` is `false`, or when the map's size changes in a way the 25274 * container's `ResizeObserver` cannot observe. 25275 * 25276 * Triggers the following events: `movestart`, `move`, `moveend`, and `resize`. 25277 * 25278 * @param eventData - Additional properties to be passed to `movestart`, `move`, `resize`, and `moveend` 25279 * events that get triggered as a result of resize. This can be useful for differentiating the 25280 * source of an event (for example, user-initiated or programmatically-triggered events). 25281 * @example 25282 * Resize a map with `trackResize` disabled when its container is shown after being hidden with CSS. 25283 * ```ts 25284 * let mapDiv = document.getElementById('map'); 25285 * if (mapDiv.style.visibility === 'visible') map.resize(); 25286 * ``` 25287 */ 25288 resize(eventData, constrainTransform = true) { 25289 if (this._lostContextStyle.style !== null) return this; 25290 this._resizeInternal(constrainTransform); 25291 const fireMoving = !this._camera._moving; 25292 if (fireMoving) { 25293 this.stop(); 25294 this.fire(new MapMovementEvent("movestart", eventData)).fire(new MapMovementEvent("move", eventData)); 25295 } 25296 this.fire(new MapLibreEvent("resize", eventData)); 25297 if (fireMoving) this.fire(new MapMovementEvent("moveend", eventData)); 25298 return this; 25299 } 25300 /** 25301 * Resizes the map according to the dimensions of its 25302 * `container` element. 25303 * 25304 * It does not trigger any events, and does not check for context loss. 25305 * 25306 * It is used internally and by {@link Map.resize}. 25307 * 25308 * @internal 25309 * 25310 * @param constrainTransform - whether to constrain the transform after resizing. 25311 */ 25312 _resizeInternal(constrainTransform = true) { 25313 const [width, height] = this._containerDimensions(); 25314 const clampedPixelRatio = this._getClampedPixelRatio(width, height); 25315 this._resizeCanvas(width, height, clampedPixelRatio); 25316 this.painter.resize(width, height, clampedPixelRatio); 25317 if (this.painter.overLimit()) { 25318 const gl = this.painter.context.gl; 25319 this._maxCanvasSize = [gl.drawingBufferWidth, gl.drawingBufferHeight]; 25320 const clampedPixelRatio = this._getClampedPixelRatio(width, height); 25321 this._resizeCanvas(width, height, clampedPixelRatio); 25322 this.painter.resize(width, height, clampedPixelRatio); 25323 } 25324 this._resizeTransform(constrainTransform); 25325 } 25326 _resizeTransform(constrainTransform = true) { 25327 const [width, height] = this._containerDimensions(); 25328 this._camera.transform.resize(width, height, constrainTransform); 25329 this._camera._requestedCameraState?.resize(width, height, constrainTransform); 25330 } 25331 /** 25332 * @internal 25333 * Return the map's pixel ratio eventually scaled down to respect maxCanvasSize. 25334 * Internally you should use this and not getPixelRatio(). 25335 * Warns once when the ratio is scaled down. The message carries no sizes: this runs on every 25336 * resize and `warnOnce` de-duplicates by message, so sizes would warn on every drag frame. 25337 */ 25338 _getClampedPixelRatio(width, height) { 25339 const { 0: maxCanvasWidth, 1: maxCanvasHeight } = this._maxCanvasSize; 25340 const pixelRatio = this.getPixelRatio(); 25341 const canvasWidth = width * pixelRatio; 25342 const canvasHeight = height * pixelRatio; 25343 const widthScaleFactor = canvasWidth > maxCanvasWidth ? maxCanvasWidth / canvasWidth : 1; 25344 const heightScaleFactor = canvasHeight > maxCanvasHeight ? maxCanvasHeight / canvasHeight : 1; 25345 const scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); 25346 if (scaleFactor < 1) warnOnce("The canvas is larger than maxCanvasSize and is rendered at a lower pixel ratio to fit. Increase maxCanvasSize, within MAX_TEXTURE_SIZE, to render at full resolution."); 25347 return scaleFactor * pixelRatio; 25348 } 25349 /** 25350 * Returns the map's pixel ratio. 25351 * Note that the pixel ratio actually applied may be lower to respect maxCanvasSize. 25352 * @returns The pixel ratio. 25353 */ 25354 getPixelRatio() { 25355 return this._overridePixelRatio ?? devicePixelRatio; 25356 } 25357 /** 25358 * Sets the map's pixel ratio. This allows to override `devicePixelRatio`. 25359 * After this call, the canvas' `width` attribute will be `container.clientWidth * pixelRatio` 25360 * and its height attribute will be `container.clientHeight * pixelRatio`. 25361 * Set this to null to disable `devicePixelRatio` override. 25362 * Note that the pixel ratio actually applied may be lower to respect maxCanvasSize. 25363 * @param pixelRatio - The pixel ratio. 25364 */ 25365 setPixelRatio(pixelRatio) { 25366 this._overridePixelRatio = pixelRatio; 25367 this.resize(); 25368 } 25369 /** 25370 * Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not 25371 * an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region. 25372 * @returns The geographical bounds of the map as {@link LngLatBounds}. 25373 * @example 25374 * ```ts 25375 * let bounds = map.getBounds(); 25376 * ``` 25377 */ 25378 getBounds() { 25379 return this._camera.transform.getBounds(); 25380 } 25381 /** 25382 * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. 25383 * @returns The map object. 25384 * @example 25385 * ```ts 25386 * let maxBounds = map.getMaxBounds(); 25387 * ``` 25388 */ 25389 getMaxBounds() { 25390 return this._camera.transform.getMaxBounds(); 25391 } 25392 /** 25393 * Sets or clears the map's geographical bounds. 25394 * 25395 * Pan and zoom operations are constrained within these bounds. 25396 * If a pan or zoom is performed that would 25397 * display regions outside these bounds, the map will 25398 * instead display a position and zoom level 25399 * as close as possible to the operation's request while still 25400 * remaining within the bounds. 25401 * 25402 * @param bounds - The maximum bounds to set. If `null` or `undefined` is provided, the function removes the map's maximum bounds. 25403 * @example 25404 * Define bounds that conform to the `LngLatBoundsLike` object as set the max bounds. 25405 * ```ts 25406 * let bounds = [ 25407 * [-74.04728, 40.68392], // [west, south] 25408 * [-73.91058, 40.87764] // [east, north] 25409 * ]; 25410 * map.setMaxBounds(bounds); 25411 * ``` 25412 */ 25413 setMaxBounds(bounds) { 25414 this._camera.transform.setMaxBounds(LngLatBounds.convert(bounds)); 25415 return this._update(); 25416 } 25417 /** 25418 * Sets or clears the map's minimum zoom level. 25419 * If the map's current zoom level is lower than the new minimum, 25420 * the map will zoom to the new minimum and trigger the following events: 25421 * `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 25422 * 25423 * It is not always possible to zoom out and reach the set `minZoom`. 25424 * Other factors such as map height may restrict zooming. For example, 25425 * if the map is 512px tall it will not be possible to zoom below zoom 0 25426 * no matter what the `minZoom` is set to. 25427 * 25428 * A {@link ErrorEvent} event will be fired if minZoom is out of bounds. 25429 * 25430 * @param minZoom - The minimum zoom level to set (-2 - 24). 25431 * If `null` or `undefined` is provided, the function removes the current minimum zoom (i.e. sets it to -2). 25432 * @example 25433 * ```ts 25434 * map.setMinZoom(12.25); 25435 * ``` 25436 */ 25437 setMinZoom(minZoom) { 25438 minZoom = minZoom === null || minZoom === void 0 ? defaultMinZoom : minZoom; 25439 if (minZoom >= defaultMinZoom && minZoom <= this._camera.transform.maxZoom) { 25440 const zoomBefore = this._camera.transform.zoom; 25441 const tr = this._camera.getTransformForUpdate(); 25442 tr.setMinZoom(minZoom); 25443 this._camera.applyUpdatedTransform(tr); 25444 this._update(); 25445 if (zoomBefore !== this._camera.transform.zoom) this.fire(new MapMovementEvent("zoomstart")).fire(new MapMovementEvent("zoom")).fire(new MapMovementEvent("zoomend")).fire(new MapMovementEvent("movestart")).fire(new MapMovementEvent("move")).fire(new MapMovementEvent("moveend")); 25446 return this; 25447 } else throw new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`); 25448 } 25449 /** 25450 * Returns the map's minimum allowable zoom level. 25451 * 25452 * @param constrained - If `true`, returns the effective minimum zoom after applying the map's viewport constraints. 25453 * If `false` or omitted, returns the configured minimum zoom. 25454 * @returns minZoom 25455 * @example 25456 * ```ts 25457 * let minZoom = map.getMinZoom(); 25458 * ``` 25459 */ 25460 getMinZoom(constrained = false) { 25461 const transform = this._camera.transform; 25462 return constrained ? transform.applyConstrain(transform.center, transform.minZoom).zoom : transform.minZoom; 25463 } 25464 /** 25465 * Sets or clears the map's maximum zoom level. 25466 * If the map's current zoom level is higher than the new maximum, 25467 * the map will zoom to the new maximum and trigger the following events: 25468 * `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, and `zoomend`. 25469 * 25470 * A {@link ErrorEvent} event will be fired if minZoom is out of bounds. 25471 * 25472 * @param maxZoom - The maximum zoom level to set. 25473 * If `null` or `undefined` is provided, the function removes the current maximum zoom (sets it to 22). 25474 * @example 25475 * ```ts 25476 * map.setMaxZoom(18.75); 25477 * ``` 25478 */ 25479 setMaxZoom(maxZoom) { 25480 maxZoom = maxZoom === null || maxZoom === void 0 ? defaultMaxZoom : maxZoom; 25481 if (maxZoom >= this._camera.transform.minZoom) { 25482 const zoomBefore = this._camera.transform.zoom; 25483 const tr = this._camera.getTransformForUpdate(); 25484 tr.setMaxZoom(maxZoom); 25485 this._camera.applyUpdatedTransform(tr); 25486 this._update(); 25487 if (zoomBefore !== this._camera.transform.zoom) this.fire(new MapMovementEvent("zoomstart")).fire(new MapMovementEvent("zoom")).fire(new MapMovementEvent("zoomend")).fire(new MapMovementEvent("movestart")).fire(new MapMovementEvent("move")).fire(new MapMovementEvent("moveend")); 25488 return this; 25489 } else throw new Error("maxZoom must be greater than the current minZoom"); 25490 } 25491 /** 25492 * Returns the map's maximum allowable zoom level. 25493 * 25494 * @returns The maxZoom 25495 * @example 25496 * ```ts 25497 * let maxZoom = map.getMaxZoom(); 25498 * ``` 25499 */ 25500 getMaxZoom() { 25501 return this._camera.transform.maxZoom; 25502 } 25503 /** 25504 * Sets or clears the map's minimum pitch. 25505 * If the map's current pitch is lower than the new minimum, 25506 * the map will pitch to the new minimum and trigger the following events: 25507 * `movestart`, `move`, `moveend`, `pitchstart`, `pitch`, and `pitchend`. 25508 * 25509 * A {@link ErrorEvent} event will be fired if minPitch is out of bounds. 25510 * 25511 * @param minPitch - The minimum pitch to set (0-180). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. 25512 * If `null` or `undefined` is provided, the function removes the current minimum pitch (i.e. sets it to 0). 25513 */ 25514 setMinPitch(minPitch) { 25515 minPitch = minPitch === null || minPitch === void 0 ? defaultMinPitch : minPitch; 25516 if (minPitch < defaultMinPitch) throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); 25517 if (minPitch >= defaultMinPitch && minPitch <= this._camera.transform.maxPitch) { 25518 const pitchBefore = this._camera.transform.pitch; 25519 const tr = this._camera.getTransformForUpdate(); 25520 tr.setMinPitch(minPitch); 25521 this._camera.applyUpdatedTransform(tr); 25522 this._update(); 25523 if (pitchBefore !== this._camera.transform.pitch) this.fire(new MapMovementEvent("pitchstart")).fire(new MapMovementEvent("pitch")).fire(new MapMovementEvent("pitchend")).fire(new MapMovementEvent("movestart")).fire(new MapMovementEvent("move")).fire(new MapMovementEvent("moveend")); 25524 return this; 25525 } else throw new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`); 25526 } 25527 /** 25528 * Returns the map's minimum allowable pitch. 25529 * 25530 * @returns The minPitch 25531 */ 25532 getMinPitch() { 25533 return this._camera.transform.minPitch; 25534 } 25535 /** 25536 * Sets or clears the map's maximum pitch. 25537 * If the map's current pitch is higher than the new maximum, 25538 * the map will pitch to the new maximum and trigger the following events: 25539 * `movestart`, `move`, `moveend`, `pitchstart`, `pitch`, and `pitchend`. 25540 * 25541 * A {@link ErrorEvent} event will be fired if maxPitch is out of bounds. 25542 * 25543 * @param maxPitch - The maximum pitch to set (0-180). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. 25544 * If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 60). 25545 */ 25546 setMaxPitch(maxPitch) { 25547 maxPitch = maxPitch === null || maxPitch === void 0 ? defaultMaxPitch : maxPitch; 25548 if (maxPitch > maxPitchThreshold) throw new Error(`maxPitch must be less than or equal to ${maxPitchThreshold}`); 25549 if (maxPitch >= this._camera.transform.minPitch) { 25550 const pitchBefore = this._camera.transform.pitch; 25551 const tr = this._camera.getTransformForUpdate(); 25552 tr.setMaxPitch(maxPitch); 25553 this._camera.applyUpdatedTransform(tr); 25554 this._update(); 25555 if (pitchBefore !== this._camera.transform.pitch) this.fire(new MapMovementEvent("pitchstart")).fire(new MapMovementEvent("pitch")).fire(new MapMovementEvent("pitchend")).fire(new MapMovementEvent("movestart")).fire(new MapMovementEvent("move")).fire(new MapMovementEvent("moveend")); 25556 return this; 25557 } else throw new Error("maxPitch must be greater than the current minPitch"); 25558 } 25559 /** 25560 * Returns the map's maximum allowable pitch. 25561 * 25562 * @returns The maxPitch 25563 */ 25564 getMaxPitch() { 25565 return this._camera.transform.maxPitch; 25566 } 25567 /** 25568 * Returns the map's anisotropic filter pitch. 25569 * If the map is pitched beyond this threshold, anisotropic filtering will be applied to all raster layers. 25570 * 25571 * @returns The anisotropicFilterPitch 25572 * @example 25573 * ```ts 25574 * let anisotropicFilterPitch = map.getAnisotropicFilterPitch(); 25575 * ``` 25576 */ 25577 getAnisotropicFilterPitch() { 25578 return this._anisotropicFilterPitch; 25579 } 25580 /** 25581 * Sets the map's anisotropic filter pitch or reverts it to its default. 25582 * 25583 * A {@link ErrorEvent} event will be fired if anisotropicFilterPitch is out of bounds. 25584 * 25585 * @param anisotropicFilterPitch - The pitch above which to apply anisotropic filtering to the map's raster layers (0-180). 25586 * If `null` or `undefined` is provided, the function reverts to the default pitch threshold (20). 25587 * 25588 * 25589 * @example 25590 * ```ts 25591 * map.setAnisotropicFilterPitch(85); 25592 * ``` 25593 */ 25594 setAnisotropicFilterPitch(anisotropicFilterPitch) { 25595 anisotropicFilterPitch = anisotropicFilterPitch === null || anisotropicFilterPitch === void 0 ? defaultAnisotropicFilterPitch : anisotropicFilterPitch; 25596 if (anisotropicFilterPitch > maxPitchThreshold) throw new Error(`anisotropicFilterPitch must be less than or equal to ${maxPitchThreshold}`); 25597 if (anisotropicFilterPitch < defaultMinPitch) throw new Error(`anisotropicFilterPitch must be greater than or equal to ${defaultMinPitch}`); 25598 this._anisotropicFilterPitch = anisotropicFilterPitch; 25599 return this._update(); 25600 } 25601 /** 25602 * Returns the state of `renderWorldCopies`. If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: 25603 * 25604 * - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire 25605 * container, there will be blank space beyond 180 and -180 degrees longitude. 25606 * - Features that cross 180 and -180 degrees longitude will be cut in two (with one portion on the right edge of the 25607 * map and the other on the left edge of the map) at every zoom level. 25608 * @returns The renderWorldCopies 25609 * @example 25610 * ```ts 25611 * let worldCopiesRendered = map.getRenderWorldCopies(); 25612 * ``` 25613 * @see [Render world copies](https://maplibre.org/maplibre-gl-js/docs/examples/render-world-copies/) 25614 */ 25615 getRenderWorldCopies() { 25616 return this._camera.transform.renderWorldCopies; 25617 } 25618 /** 25619 * Sets the state of `renderWorldCopies`. 25620 * 25621 * @param renderWorldCopies - If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: 25622 * 25623 * - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire 25624 * container, there will be blank space beyond 180 and -180 degrees longitude. 25625 * - Features that cross 180 and -180 degrees longitude will be cut in two (with one portion on the right edge of the 25626 * map and the other on the left edge of the map) at every zoom level. 25627 * 25628 * `undefined` is treated as `true`, `null` is treated as `false`. 25629 * @example 25630 * ```ts 25631 * map.setRenderWorldCopies(true); 25632 * ``` 25633 * @see [Render world copies](https://maplibre.org/maplibre-gl-js/docs/examples/render-world-copies/) 25634 */ 25635 setRenderWorldCopies(renderWorldCopies) { 25636 this._camera.transform.setRenderWorldCopies(renderWorldCopies); 25637 return this._update(); 25638 } 25639 /** Sets or clears the callback overriding how the map constrains the viewport's lnglat and zoom to respect the longitude and latitude bounds. 25640 * 25641 * @param constrain - A {@link TransformConstrainFunction} callback defining how the viewport should respect the bounds. 25642 * 25643 * `null` clears the callback and reverts the constrain to the map transform's default constrain function. 25644 * @example 25645 * ```ts 25646 * function customTransformConstrain(lngLat, zoom) { 25647 * return {center: lngLat, zoom: zoom ?? 0}; 25648 * }; 25649 * map.setTransformConstrain(customTransformConstrain); 25650 * ``` 25651 * @see [Customize the map transform constrain](https://maplibre.org/maplibre-gl-js/docs/examples/customize-the-map-transform-constrain/) 25652 */ 25653 setTransformConstrain(constrain) { 25654 this._camera.transform.setConstrainOverride(constrain); 25655 return this._update(); 25656 } 25657 /** 25658 * Returns a [Point](https://github.com/mapbox/point-geometry) representing pixel coordinates, relative to the map's `container`, 25659 * that correspond to the specified geographical location. 25660 * 25661 * A location behind the camera has no corresponding pixel. For such a location the 25662 * returned point is outside the viewport, on the side through which the location left 25663 * the screen, one viewport width or height away from the edge. 25664 * 25665 * @param lnglat - The geographical location to project. 25666 * @returns The [Point](https://github.com/mapbox/point-geometry) corresponding to `lnglat`, relative to the map's `container`. 25667 * @example 25668 * ```ts 25669 * let coordinate = [-122.420679, 37.772537]; 25670 * let point = map.project(coordinate); 25671 * ``` 25672 */ 25673 project(lnglat) { 25674 return this._camera.transform.locationToScreenPoint(LngLat.convert(lnglat), this.style && this.terrain); 25675 } 25676 /** 25677 * Returns a {@link LngLat} representing geographical coordinates that correspond 25678 * to the specified pixel coordinates. 25679 * 25680 * @param point - The pixel coordinates to unproject. 25681 * @returns The {@link LngLat} corresponding to `point`. 25682 * @example 25683 * ```ts 25684 * map.on('click', (e) => { 25685 * // When the map is clicked, get the geographic coordinate. 25686 * let coordinate = map.unproject(e.point); 25687 * }); 25688 * ``` 25689 */ 25690 unproject(point) { 25691 return this._camera.transform.screenPointToLocation(Point.convert(point), this.terrain); 25692 } 25693 /** 25694 * Returns true if the map is panning, zooming, rotating, or pitching due to a camera animation or user gesture. 25695 * @returns true if the map is moving. 25696 * @example 25697 * ```ts 25698 * let isMoving = map.isMoving(); 25699 * ``` 25700 */ 25701 isMoving() { 25702 return this._camera.isMoving() || this._handlers?.isMoving() || false; 25703 } 25704 /** 25705 * Returns true if the map is zooming due to a camera animation or user gesture. 25706 * @returns true if the map is zooming. 25707 * @example 25708 * ```ts 25709 * let isZooming = map.isZooming(); 25710 * ``` 25711 */ 25712 isZooming() { 25713 return this._camera.isZooming() || this._handlers?.isZooming() || false; 25714 } 25715 /** 25716 * Returns true if the map is rotating due to a camera animation or user gesture. 25717 * @returns true if the map is rotating. 25718 * @example 25719 * ```ts 25720 * map.isRotating(); 25721 * ``` 25722 */ 25723 isRotating() { 25724 return this._camera.isRotating() || this._handlers?.isRotating() || false; 25725 } 25726 _createDelegatedListener(type, layerIds, listener) { 25727 if (type === "mouseenter" || type === "mouseover") { 25728 let mousein = false; 25729 const mousemove = (e) => { 25730 const existingLayers = layerIds.filter((layerId) => this.getLayer(layerId)); 25731 const features = existingLayers.length !== 0 ? this.queryRenderedFeatures(e.point, { layers: existingLayers }) : []; 25732 if (!features.length) mousein = false; 25733 else if (!mousein) { 25734 mousein = true; 25735 listener.call(this, new MapMouseEvent(type, this, e.originalEvent, { features })); 25736 } 25737 }; 25738 const mouseout = () => { 25739 mousein = false; 25740 }; 25741 return { 25742 layers: layerIds, 25743 listener, 25744 delegates: { 25745 mousemove, 25746 mouseout 25747 } 25748 }; 25749 } else if (type === "mouseleave" || type === "mouseout") { 25750 let mousein = false; 25751 const mousemove = (e) => { 25752 const existingLayers = layerIds.filter((layerId) => this.getLayer(layerId)); 25753 if ((existingLayers.length !== 0 ? this.queryRenderedFeatures(e.point, { layers: existingLayers }) : []).length) mousein = true; 25754 else if (mousein) { 25755 mousein = false; 25756 listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); 25757 } 25758 }; 25759 const mouseout = (e) => { 25760 if (mousein) { 25761 mousein = false; 25762 listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); 25763 } 25764 }; 25765 return { 25766 layers: layerIds, 25767 listener, 25768 delegates: { 25769 mousemove, 25770 mouseout 25771 } 25772 }; 25773 } else { 25774 const delegate = (e) => { 25775 const existingLayers = layerIds.filter((layerId) => this.getLayer(layerId)); 25776 const features = existingLayers.length !== 0 ? this.queryRenderedFeatures(e.point, { layers: existingLayers }) : []; 25777 if (features.length) { 25778 e.features = features; 25779 listener.call(this, e); 25780 delete e.features; 25781 } 25782 }; 25783 return { 25784 layers: layerIds, 25785 listener, 25786 delegates: { [type]: delegate } 25787 }; 25788 } 25789 } 25790 _saveDelegatedListener(type, delegatedListener) { 25791 this._delegatedListeners ||= {}; 25792 this._delegatedListeners[type] ||= []; 25793 this._delegatedListeners[type].push(delegatedListener); 25794 } 25795 _removeDelegatedListener(type, layerIds, listener) { 25796 if (!this._delegatedListeners?.[type]) return; 25797 const listeners = this._delegatedListeners[type]; 25798 for (let i = 0; i < listeners.length; i++) { 25799 const delegatedListener = listeners[i]; 25800 if (delegatedListener.listener === listener && delegatedListener.layers.length === layerIds.length && delegatedListener.layers.every((layerId) => layerIds.includes(layerId))) { 25801 for (const event in delegatedListener.delegates) this.off(event, delegatedListener.delegates[event]); 25802 listeners.splice(i, 1); 25803 return; 25804 } 25805 } 25806 } 25807 on(type, layerIdsOrListener, listener) { 25808 if (listener === void 0) return super.on(type, layerIdsOrListener); 25809 const layerIds = typeof layerIdsOrListener === "string" ? [layerIdsOrListener] : layerIdsOrListener; 25810 const delegatedListener = this._createDelegatedListener(type, layerIds, listener); 25811 this._saveDelegatedListener(type, delegatedListener); 25812 for (const event in delegatedListener.delegates) this.on(event, delegatedListener.delegates[event]); 25813 return { unsubscribe: () => { 25814 this._removeDelegatedListener(type, layerIds, listener); 25815 } }; 25816 } 25817 once(type, layerIdsOrListener, listener) { 25818 if (listener === void 0) return super.once(type, layerIdsOrListener); 25819 const layerIds = typeof layerIdsOrListener === "string" ? [layerIdsOrListener] : layerIdsOrListener; 25820 const delegatedListener = this._createDelegatedListener(type, layerIds, listener); 25821 for (const key in delegatedListener.delegates) { 25822 const delegate = delegatedListener.delegates[key]; 25823 delegatedListener.delegates[key] = (...args) => { 25824 this._removeDelegatedListener(type, layerIds, listener); 25825 delegate(...args); 25826 }; 25827 } 25828 this._saveDelegatedListener(type, delegatedListener); 25829 for (const event in delegatedListener.delegates) this.once(event, delegatedListener.delegates[event]); 25830 return this; 25831 } 25832 off(type, layerIdsOrListener, listener) { 25833 if (listener === void 0) return super.off(type, layerIdsOrListener); 25834 const layerIds = typeof layerIdsOrListener === "string" ? [layerIdsOrListener] : layerIdsOrListener; 25835 this._removeDelegatedListener(type, layerIds, listener); 25836 return this; 25837 } 25838 /** 25839 * Returns an array of MapGeoJSONFeature objects 25840 * representing visible features that satisfy the query parameters. 25841 * 25842 * @param geometryOrOptions - (optional) The geometry of the query region in pixel points within the map viewport: 25843 * either a single pixel point or a pair of top-left and bottom-right pixel points describing a bounding box. 25844 * The origin of the pixel points is at the top-left of the map viewport. 25845 * Omitting this parameter (i.e. calling {@link Map.queryRenderedFeatures} with zero arguments, 25846 * or with only a `options` argument) is equivalent to passing a bounding box encompassing the entire 25847 * map viewport. 25848 * The geometryOrOptions can receive a {@link QueryRenderedFeaturesOptions} only to support a situation where the function receives only one parameter which is the options parameter. 25849 * @param options - (optional) Options object. 25850 * 25851 * @returns An array of MapGeoJSONFeature objects. 25852 * 25853 * The `properties` value of each returned feature object contains the properties of its source feature. For GeoJSON sources, only 25854 * string and numeric property values are supported (i.e. `null`, `Array`, and `Object` values are not supported). 25855 * 25856 * Each feature includes top-level `layer`, `source`, and `sourceLayer` properties. The `layer` property is an object 25857 * representing the style layer to which the feature belongs. Layout and paint properties in this object contain values 25858 * which are fully evaluated for the given zoom level and feature. 25859 * 25860 * Only features that are currently rendered are included. Some features will **not** be included, like: 25861 * 25862 * - Features from layers whose `visibility` property is `"none"`. 25863 * - Features from layers whose zoom range excludes the current zoom level. 25864 * - Symbol features that have been hidden due to text or icon collision. 25865 * 25866 * Features from all other layers are included, including features that may have no visible 25867 * contribution to the rendered result; for example, because the layer's opacity or color alpha component is set to 25868 * 0. 25869 * 25870 * The topmost rendered feature appears first in the returned array, and subsequent features are sorted by 25871 * descending z-order. Features that are rendered multiple times (due to wrapping across the antemeridian at low 25872 * zoom levels) are returned only once (though subject to the following caveat). 25873 * 25874 * Because features come from tiled vector data or GeoJSON data that is converted to tiles internally, feature 25875 * geometries may be split or duplicated across tile boundaries and, as a result, features may appear multiple 25876 * times in query results. For example, suppose there is a highway running through the bounding rectangle of a query. 25877 * The results of the query will be those parts of the highway that lie within the map tiles covering the bounding 25878 * rectangle, even if the highway extends into other tiles, and the portion of the highway within each map tile 25879 * will be returned as a separate feature. Similarly, a point feature near a tile boundary may appear in multiple 25880 * tiles due to tile buffering. 25881 * 25882 * @example 25883 * Find all features at a point 25884 * ```ts 25885 * let features = map.queryRenderedFeatures( 25886 * [20, 35], 25887 * { layers: ['my-layer-name'] } 25888 * ); 25889 * ``` 25890 * 25891 * @example 25892 * Find all features within a static bounding box 25893 * ```ts 25894 * let features = map.queryRenderedFeatures( 25895 * [[10, 20], [30, 50]], 25896 * { layers: ['my-layer-name'] } 25897 * ); 25898 * ``` 25899 * 25900 * @example 25901 * Find all features within a bounding box around a point 25902 * ```ts 25903 * let width = 10; 25904 * let height = 20; 25905 * let features = map.queryRenderedFeatures([ 25906 * [point.x - width / 2, point.y - height / 2], 25907 * [point.x + width / 2, point.y + height / 2] 25908 * ], { layers: ['my-layer-name'] }); 25909 * ``` 25910 * 25911 * @example 25912 * Query all rendered features from a single layer 25913 * ```ts 25914 * let features = map.queryRenderedFeatures({ layers: ['my-layer-name'] }); 25915 * ``` 25916 * @see [Get features under the mouse pointer](https://maplibre.org/maplibre-gl-js/docs/examples/get-features-under-the-mouse-pointer/) 25917 */ 25918 queryRenderedFeatures(geometryOrOptions, options) { 25919 if (!this.style) return []; 25920 let queryGeometry; 25921 const isGeometry = geometryOrOptions instanceof Point || Array.isArray(geometryOrOptions); 25922 const geometry = isGeometry ? geometryOrOptions : [[0, 0], [this._camera.transform.width, this._camera.transform.height]]; 25923 options ||= (isGeometry ? {} : geometryOrOptions) || {}; 25924 if (geometry instanceof Point || typeof geometry[0] === "number") queryGeometry = [Point.convert(geometry)]; 25925 else { 25926 const tl = Point.convert(geometry[0]); 25927 const br = Point.convert(geometry[1]); 25928 queryGeometry = [ 25929 tl, 25930 new Point(br.x, tl.y), 25931 br, 25932 new Point(tl.x, br.y), 25933 tl 25934 ]; 25935 } 25936 return this.style.queryRenderedFeatures(queryGeometry, options, this._camera.transform); 25937 } 25938 /** 25939 * Returns an array of MapGeoJSONFeature objects 25940 * representing features within the specified vector tile or GeoJSON source that satisfy the query parameters. 25941 * 25942 * @param sourceId - The ID of the vector tile or GeoJSON source to query. 25943 * @param parameters - The options object. 25944 * @returns An array of MapGeoJSONFeature objects. 25945 * 25946 * In contrast to {@link Map.queryRenderedFeatures}, this function returns all features matching the query parameters, 25947 * whether or not they are rendered by the current style (i.e. visible). The domain of the query includes all currently-loaded 25948 * vector tiles and GeoJSON source tiles: this function does not check tiles outside the currently 25949 * visible viewport. 25950 * 25951 * Because features come from tiled vector data or GeoJSON data that is converted to tiles internally, feature 25952 * geometries may be split or duplicated across tile boundaries and, as a result, features may appear multiple 25953 * times in query results. For example, suppose there is a highway running through the bounding rectangle of a query. 25954 * The results of the query will be those parts of the highway that lie within the map tiles covering the bounding 25955 * rectangle, even if the highway extends into other tiles, and the portion of the highway within each map tile 25956 * will be returned as a separate feature. Similarly, a point feature near a tile boundary may appear in multiple 25957 * tiles due to tile buffering. 25958 * 25959 * @example 25960 * Find all features in one source layer in a vector source 25961 * ```ts 25962 * let features = map.querySourceFeatures('your-source-id', { 25963 * sourceLayer: 'your-source-layer' 25964 * }); 25965 * ``` 25966 * 25967 */ 25968 querySourceFeatures(sourceId, parameters) { 25969 return this.style.querySourceFeatures(sourceId, parameters); 25970 } 25971 /** 25972 * Updates the map's MapLibre style object with a new value. 25973 * 25974 * If a style is already set when this is used and options.diff is set to true, the map renderer will attempt to compare the given style 25975 * against the map's current state and perform only the changes necessary to make the map style match the desired state. Changes in sprites 25976 * (images used for icons and patterns) and glyphs (fonts for label text) **cannot** be diffed. If the sprites or fonts used in the current 25977 * style and the given style are different in any way, the map renderer will force a full update, removing the current style and building 25978 * the given one from scratch. 25979 * 25980 * 25981 * @param style - A JSON object conforming to the schema described in the 25982 * [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/), or a URL to such JSON. 25983 * @param options - The options object. 25984 * 25985 * @example 25986 * ```ts 25987 * map.setStyle("https://demotiles.maplibre.org/style.json"); 25988 * 25989 * map.setStyle('https://demotiles.maplibre.org/style.json', { 25990 * transformStyle: (previousStyle, nextStyle) => ({ 25991 * ...nextStyle, 25992 * sources: { 25993 * ...nextStyle.sources, 25994 * // copy a source from previous style 25995 * 'osm': previousStyle.sources.osm 25996 * }, 25997 * layers: [ 25998 * // background layer 25999 * nextStyle.layers[0], 26000 * // copy a layer from previous style 26001 * previousStyle.layers[0], 26002 * // other layers from the next style 26003 * ...nextStyle.layers.slice(1).map(layer => { 26004 * // hide the layers we don't need from demotiles style 26005 * if (layer.id.startsWith('geolines')) { 26006 * layer.layout = {...layer.layout || {}, visibility: 'none'}; 26007 * // filter out US polygons 26008 * } else if (layer.id.startsWith('coastline') || layer.id.startsWith('countries')) { 26009 * layer.filter = ['!=', ['get', 'ADM0_A3'], 'USA']; 26010 * } 26011 * return layer; 26012 * }) 26013 * ] 26014 * }) 26015 * }); 26016 * ``` 26017 */ 26018 setStyle(style, options) { 26019 options = extend({}, { 26020 localIdeographFontFamily: this._localIdeographFontFamily, 26021 validate: this._validateStyle 26022 }, options); 26023 this._styleUrl = typeof style === "string" ? style : null; 26024 if (options.diff !== false && options.localIdeographFontFamily === this._localIdeographFontFamily && this.style && style) { 26025 this._diffStyle(style, options); 26026 return this; 26027 } else { 26028 this._localIdeographFontFamily = options.localIdeographFontFamily; 26029 return this._updateStyle(style, options); 26030 } 26031 } 26032 /** 26033 * Updates the requestManager's transform request with a new function 26034 * 26035 * @param transformRequest - A callback run before the Map makes a request for an external URL. The callback can be used to modify the url, set headers, or set the credentials property for cross-origin requests. 26036 * Expected to return an object with a `url` property and optionally `headers` and `credentials` properties 26037 * 26038 * @example 26039 * ```ts 26040 * map.setTransformRequest((url: string, resourceType: string) => {}); 26041 * ``` 26042 */ 26043 setTransformRequest(transformRequest) { 26044 this._requestManager.setTransformRequest(transformRequest); 26045 return this; 26046 } 26047 _getUIString(key) { 26048 const str = this._locale[key]; 26049 if (str == null) throw new Error(`Missing UI string '${key}'`); 26050 return str; 26051 } 26052 _updateStyle(style, options) { 26053 this._diffStyleRequest?.abort(); 26054 this._diffStyleRequest = null; 26055 if (options.transformStyle && this.style && !this.style._loaded) { 26056 this.style.once("style.load", () => this._updateStyle(style, options)); 26057 return; 26058 } 26059 const previousStyle = this.style && options.transformStyle ? this.style.serialize() : void 0; 26060 if (this.style) { 26061 this.style.setEventedParent(null); 26062 this.style._remove(!style); 26063 } 26064 if (!style) { 26065 if (this._frameRequest) { 26066 this._frameRequest.abort(); 26067 this._frameRequest = null; 26068 } 26069 this.style?.projection?.destroy(); 26070 delete this.style; 26071 return this; 26072 } else this.style = new Style(this, options || {}); 26073 this.style.setEventedParent(this, { style: this.style }); 26074 if (typeof style === "string") this.style.loadURL(style, options, previousStyle); 26075 else this.style.loadJSON(style, options, previousStyle); 26076 return this; 26077 } 26078 _lazyInitEmptyStyle() { 26079 if (!this.style) { 26080 this.style = new Style(this, {}); 26081 this.style.setEventedParent(this, { style: this.style }); 26082 this.style.loadEmpty(); 26083 } 26084 } 26085 async _diffStyle(style, options) { 26086 this._diffStyleRequest?.abort(); 26087 if (typeof style === "string") { 26088 const url = style; 26089 this._diffStyleRequest = new AbortController(); 26090 const abortController = this._diffStyleRequest; 26091 try { 26092 const request = await this._requestManager.transformRequest(url, "Style"); 26093 if (abortController.signal.aborted) { 26094 this._diffStyleRequest = null; 26095 return; 26096 } 26097 const response = await getJSON(request, abortController); 26098 this._diffStyleRequest = null; 26099 this._updateDiff(response.data, options); 26100 } catch (error) { 26101 this._diffStyleRequest = null; 26102 if (!isAbortError(error)) this.fire(new ErrorEvent(ensureError(error))); 26103 } 26104 } else if (typeof style === "object") { 26105 this._diffStyleRequest = null; 26106 this._updateDiff(style, options); 26107 } 26108 } 26109 _updateDiff(style, options) { 26110 try { 26111 if (this.style.setState(style, options)) this._update(true); 26112 } catch (e) { 26113 warnOnce(`Unable to perform style diff: ${ensureError(e).message}. Rebuilding the style from scratch.`); 26114 this._updateStyle(style, options); 26115 } 26116 } 26117 /** 26118 * Returns the map's MapLibre style object, a JSON object which can be used to recreate the map's style. 26119 * 26120 * @returns The map's style JSON object. 26121 * 26122 * @example 26123 * ```ts 26124 * let styleJson = map.getStyle(); 26125 * ``` 26126 * 26127 */ 26128 getStyle() { 26129 if (this.style) return this.style.serialize(); 26130 } 26131 /** 26132 * Returns the URL the map's style was loaded from. 26133 * 26134 * @returns The URL given to {@link Map.setStyle} or the `style` map option, or `null` when the style was given as an object or the map has no style. 26135 * 26136 * @example 26137 * ```ts 26138 * const styleUrl = map.getStyleUrl(); 26139 * ``` 26140 */ 26141 getStyleUrl() { 26142 return this._styleUrl; 26143 } 26144 /** 26145 * @internal 26146 * Returns the map's style and cloned images to restore context. 26147 * @returns An object containing the style and images. 26148 */ 26149 _getStyleAndImages() { 26150 if (this.style) return { 26151 style: this.style.serialize(), 26152 images: this.style.imageManager.cloneImages() 26153 }; 26154 return { 26155 style: null, 26156 images: {} 26157 }; 26158 } 26159 /** 26160 * Returns a Boolean indicating whether the map's style is fully loaded. 26161 * 26162 * @returns A Boolean indicating whether the style is fully loaded. 26163 * 26164 * @example 26165 * ```ts 26166 * let styleLoadStatus = map.isStyleLoaded(); 26167 * ``` 26168 */ 26169 isStyleLoaded() { 26170 if (!this.style) { 26171 warnOnce("There is no style added to the map."); 26172 return; 26173 } 26174 return this.style.loaded(); 26175 } 26176 /** 26177 * Adds a source to the map's style. 26178 * 26179 * Events triggered: 26180 * 26181 * Triggers the `source.add` event. 26182 * 26183 * @param id - The ID of the source to add. Must not conflict with existing sources. 26184 * @param source - The source object, conforming to the 26185 * MapLibre Style Specification's [source definition](https://maplibre.org/maplibre-style-spec/sources) or 26186 * {@link CanvasSourceSpecification}. 26187 * @example 26188 * ```ts 26189 * map.addSource('my-data', { 26190 * type: 'vector', 26191 * url: 'https://demotiles.maplibre.org/tiles/tiles.json' 26192 * }); 26193 * ``` 26194 * @example 26195 * ```ts 26196 * map.addSource('my-data', { 26197 * "type": "geojson", 26198 * "data": { 26199 * "type": "Feature", 26200 * "geometry": { 26201 * "type": "Point", 26202 * "coordinates": [-77.0396, 38.8891] 26203 * }, 26204 * "properties": { 26205 * "title": "Washington DC", 26206 * "marker-symbol": "monument" 26207 * } 26208 * } 26209 * }); 26210 * ``` 26211 * @see GeoJSON source: [Add live realtime data](https://maplibre.org/maplibre-gl-js/docs/examples/add-live-realtime-data/) 26212 */ 26213 addSource(id, source) { 26214 this._lazyInitEmptyStyle(); 26215 this.style.addSource(id, source); 26216 return this._update(true); 26217 } 26218 /** 26219 * Returns a Boolean indicating whether the source is loaded. Returns `true` if the source with 26220 * the given ID in the map's style has no outstanding network requests, otherwise `false`. 26221 * 26222 * A {@link ErrorEvent} event will be fired if there is no source with the specified ID. 26223 * 26224 * @param id - The ID of the source to be checked. 26225 * @returns A Boolean indicating whether the source is loaded. 26226 * @example 26227 * ```ts 26228 * let sourceLoaded = map.isSourceLoaded('bathymetry-data'); 26229 * ``` 26230 */ 26231 isSourceLoaded(id) { 26232 const tileManager = this.style?.tileManagers[id]; 26233 if (tileManager === void 0) { 26234 this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`There is no tile manager with ID '${id}'`))); 26235 return; 26236 } 26237 return tileManager.loaded(); 26238 } 26239 /** 26240 * Loads a 3D terrain mesh, based on a "raster-dem" source. 26241 * 26242 * Triggers the `terrain` event. 26243 * 26244 * @param options - Options object. 26245 * @example 26246 * ```ts 26247 * map.setTerrain({ source: 'terrain' }); 26248 * ``` 26249 */ 26250 setTerrain(options, styleOptions = {}) { 26251 this.style._checkLoaded(); 26252 if (options && validateAndEmit(this, validateStyle.terrain, { value: options }, styleOptions)) return this; 26253 if (this._terrainDataCallback) this.style.off("data", this._terrainDataCallback); 26254 if (!options) { 26255 if (this.terrain) this.terrain.destroy(); 26256 this.terrain = null; 26257 this.painter.renderToTexture = null; 26258 this.painter.destroyRTTResources(); 26259 this._camera.setTerrain(null); 26260 } else { 26261 const tileManager = this.style.tileManagers[options.source]; 26262 if (!tileManager) throw new Error(`cannot load terrain, because there exists no source with ID: ${options.source}`); 26263 if (this.terrain === null) tileManager.reload(); 26264 for (const index in this.style._layers) { 26265 const thisLayer = this.style._layers[index]; 26266 if (thisLayer.type === "hillshade" && thisLayer.source === options.source) warnOnce("You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality."); 26267 if (thisLayer.type === "color-relief" && thisLayer.source === options.source) warnOnce("You are using the same source for a color-relief layer and for 3D terrain. Please consider using two separate sources to improve rendering quality."); 26268 } 26269 if (this.terrain) this.terrain.destroy(); 26270 this.terrain = new Terrain(this.painter, tileManager, options, this._terrainSkirtLength); 26271 this.painter.renderToTexture = new RenderToTexture(this.painter, this.terrain); 26272 this._camera.setTerrain(this.terrain); 26273 this._terrainDataCallback = (e) => this._handleTerrainDataEvent(e, options.source); 26274 this.style.on("data", this._terrainDataCallback); 26275 } 26276 this.style.triggerSymbolPlacement(); 26277 this.fire(new MapTerrainEvent({ terrain: options })); 26278 return this; 26279 } 26280 _handleTerrainDataEvent(event, terrainSourceId) { 26281 if (event.dataType === "style") { 26282 this.terrain.tileManager.releaseAllRTT(); 26283 return; 26284 } 26285 const isTerrainSourceEvent = event.sourceId === terrainSourceId; 26286 if (isTerrainSourceEvent) { 26287 this.terrain.resetElevationCache(); 26288 this.style.triggerSymbolPlacement(); 26289 } 26290 if (isTerrainSourceEvent && event.tile) { 26291 this.painter.markTerrainDepthDirty(); 26292 this._camera.applyTerrainChange(); 26293 } 26294 if (!event.tile) return; 26295 if (event.source?.type === "image") { 26296 this.terrain.tileManager.releaseAllRTT(); 26297 return; 26298 } 26299 this.terrain.tileManager.releaseRTT(event.tile.tileID); 26300 } 26301 /** 26302 * Get the terrain-options if terrain is loaded 26303 * @returns the TerrainSpecification passed to setTerrain 26304 * @example 26305 * ```ts 26306 * map.getTerrain(); // { source: 'terrain' }; 26307 * ``` 26308 */ 26309 getTerrain() { 26310 return this.terrain?.options ?? null; 26311 } 26312 /** 26313 * Returns a Boolean indicating whether all tiles in the viewport from all sources on 26314 * the style are loaded. 26315 * 26316 * @returns A Boolean indicating whether all tiles are loaded. 26317 * @example 26318 * ```ts 26319 * let tilesLoaded = map.areTilesLoaded(); 26320 * ``` 26321 */ 26322 areTilesLoaded() { 26323 const tileManagers = this.style?.tileManagers; 26324 for (const tileManager of Object.values(tileManagers)) if (!tileManager.areTilesLoaded()) return false; 26325 return true; 26326 } 26327 /** 26328 * Removes a source from the map's style. 26329 * 26330 * @param id - The ID of the source to remove. 26331 * @example 26332 * ```ts 26333 * map.removeSource('bathymetry-data'); 26334 * ``` 26335 */ 26336 removeSource(id) { 26337 this.style.removeSource(id); 26338 return this._update(true); 26339 } 26340 /** 26341 * Returns the source with the specified ID in the map's style. 26342 * 26343 * This method is often used to update a source using the instance members for the relevant 26344 * source type as defined in classes that derive from {@link Source}. 26345 * For example, setting the `data` for a GeoJSON source or updating the `url` and `coordinates` 26346 * of an image source. 26347 * 26348 * @param id - The ID of the source to get. 26349 * @returns The style source with the specified ID or `undefined` if the ID 26350 * corresponds to no existing sources. 26351 * The shape of the object varies by source type. 26352 * A list of options for each source type is available on the MapLibre Style Specification's 26353 * [Sources](https://maplibre.org/maplibre-style-spec/sources/) page. 26354 * @example 26355 * ```ts 26356 * let sourceObject = map.getSource('points'); 26357 * ``` 26358 * @see [Create a draggable point](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-point/) 26359 * @see [Animate a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-a-point/) 26360 * @see [Add live realtime data](https://maplibre.org/maplibre-gl-js/docs/examples/add-live-realtime-data/) 26361 */ 26362 getSource(id) { 26363 return this.style?.getSource(id); 26364 } 26365 /** 26366 * Change the tile Level of Detail behavior of the specified source. These parameters have no effect when 26367 * pitch == 0, and the largest effect when the horizon is visible on screen. 26368 * 26369 * @param maxZoomLevelsOnScreen - The maximum number of distinct zoom levels allowed on screen at a time. 26370 * There will generally be fewer zoom levels on the screen, the maximum can only be reached when the horizon 26371 * is at the top of the screen. Increasing the maximum number of zoom levels causes the zoom level to decay 26372 * faster toward the horizon. 26373 * @param tileCountMaxMinRatio - The ratio of the maximum number of tiles loaded (at high pitch) to the minimum 26374 * number of tiles loaded. Increasing this ratio allows more tiles to be loaded at high pitch angles. If the ratio 26375 * would otherwise be exceeded, the zoom level is reduced uniformly to keep the number of tiles within the limit. 26376 * @param sourceId - The ID of the source to set tile LOD parameters for. All sources will be updated if unspecified. 26377 * If `sourceId` is specified but a corresponding source does not exist, an error is thrown. 26378 * @example 26379 * ```ts 26380 * map.setSourceTileLodParams(4.0, 3.0, 'terrain'); 26381 * ``` 26382 * @see [Modify Level of Detail behavior](https://maplibre.org/maplibre-gl-js/docs/examples/level-of-detail-control/) 26383 26384 */ 26385 setSourceTileLodParams(maxZoomLevelsOnScreen, tileCountMaxMinRatio, sourceId) { 26386 if (sourceId) { 26387 const source = this.getSource(sourceId); 26388 if (!source) throw new Error(`There is no source with ID "${sourceId}", cannot set LOD parameters`); 26389 source.calculateTileZoom = createCalculateTileZoomFunction(Math.max(1, maxZoomLevelsOnScreen), Math.max(1, tileCountMaxMinRatio)); 26390 } else for (const id in this.style.tileManagers) this.style.tileManagers[id].getSource().calculateTileZoom = createCalculateTileZoomFunction(Math.max(1, maxZoomLevelsOnScreen), Math.max(1, tileCountMaxMinRatio)); 26391 this._update(true); 26392 return this; 26393 } 26394 /** 26395 * Triggers a reload of the selected tiles 26396 * 26397 * @param sourceId - The ID of the source 26398 * @param tileIds - An array of tile IDs to be reloaded. If not defined, all tiles will be reloaded. 26399 * @example 26400 * ```ts 26401 * map.refreshTiles('satellite', [{x:1024, y: 1023, z: 11}, {x:1023, y: 1023, z: 11}]); 26402 * ``` 26403 */ 26404 refreshTiles(sourceId, tileIds) { 26405 const tileManager = this.style.tileManagers[sourceId]; 26406 if (!tileManager) throw new Error(`There is no tile manager with ID "${sourceId}", cannot refresh tile`); 26407 if (tileIds === void 0) tileManager.reload(true); 26408 else tileManager.refreshTiles(tileIds.map((tileId) => { 26409 return new CanonicalTileID(tileId.z, tileId.x, tileId.y); 26410 })); 26411 } 26412 /** 26413 * Add an image to the style. This image can be displayed on the map like any other icon in the style's 26414 * sprite using the image's ID with 26415 * [`icon-image`](https://maplibre.org/maplibre-style-spec/layers/#layout-symbol-icon-image), 26416 * [`background-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-background-background-pattern), 26417 * [`fill-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-fill-fill-pattern), 26418 * or [`line-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-line-line-pattern). 26419 * 26420 * A {@link ErrorEvent} event will be fired if the image parameter is invalid or there is not enough space in the sprite to add this image. 26421 * 26422 * @param id - The ID of the image. 26423 * @param image - The image as an `HTMLImageElement`, `ImageData`, `ImageBitmap` or object with `width`, `height`, and `data` 26424 * properties with the same format as `ImageData`. 26425 * @param options - Options object. 26426 * @example 26427 * ```ts 26428 * // If the style's sprite does not already contain an image with ID 'cat', 26429 * // add the image 'cat-icon.png' to the style's sprite with the ID 'cat'. 26430 * const image = await map.loadImage('https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Cat_silhouette.svg/400px-Cat_silhouette.svg.png'); 26431 * if (!map.hasImage('cat')) map.addImage('cat', image.data); 26432 * 26433 * // Add a stretchable image that can be used with `icon-text-fit` 26434 * // In this example, the image is 600px wide by 400px high. 26435 * const image = await map.loadImage('https://upload.wikimedia.org/wikipedia/commons/8/89/Black_and_White_Boxed_%28bordered%29.png'); 26436 * if (map.hasImage('border-image')) return; 26437 * map.addImage('border-image', image.data, { 26438 * content: [16, 16, 300, 384], // place text over left half of image, avoiding the 16px border 26439 * stretchX: [[16, 584]], // stretch everything horizontally except the 16px border 26440 * stretchY: [[16, 384]], // stretch everything vertically except the 16px border 26441 * }); 26442 * ``` 26443 * @see Use `HTMLImageElement`: [Add an icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-an-icon-to-the-map/) 26444 * @see Use `ImageData`: [Add a generated icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-generated-icon-to-the-map/) 26445 */ 26446 addImage(id, image, options = {}) { 26447 this._lazyInitEmptyStyle(); 26448 const styleImage = this._createStyleImage(image, options); 26449 if (!styleImage) return this; 26450 this.style.addImage(id, styleImage); 26451 if (styleImage.userImage?.onAdd) styleImage.userImage.onAdd(this, id); 26452 return this; 26453 } 26454 /** 26455 * Sets a callback that is invoked when an icon or pattern needed by the style is missing. 26456 * 26457 * The resolver typically loads or generates the image and registers it with {@link Map.addImage}. 26458 * MapLibre awaits the returned promise before treating the image as missing, so async work is 26459 * supported. If the image is still missing afterwards, the `styleimagemissing` event is fired. 26460 * 26461 * @param resolver - Callback used to resolve missing images, or `null` to remove the resolver. 26462 * @example 26463 * ```ts 26464 * map.setMissingStyleImageResolver(async (id) => { 26465 * const response = await fetch(`/icons/${id}.png`); 26466 * const image = await createImageBitmap(await response.blob()); 26467 * map.addImage(id, image, {pixelRatio: 2}); 26468 * }); 26469 * ``` 26470 */ 26471 setMissingStyleImageResolver(resolver) { 26472 this._missingStyleImageResolver = resolver; 26473 this.style?.setMissingImageResolver(resolver); 26474 return this; 26475 } 26476 _createStyleImage(image, options = {}) { 26477 const { pixelRatio = 1, sdf = false, stretchX, stretchY, content, textFitWidth, textFitHeight } = options; 26478 const version = 0; 26479 if (image instanceof HTMLImageElement || isImageBitmap(image)) { 26480 const { width, height, data } = browser.getImageData(image); 26481 return { 26482 data: new RGBAImage({ 26483 width, 26484 height 26485 }, data), 26486 pixelRatio, 26487 stretchX, 26488 stretchY, 26489 content, 26490 textFitWidth, 26491 textFitHeight, 26492 sdf, 26493 version 26494 }; 26495 } else if (image.width === void 0 || image.height === void 0) { 26496 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`"))); 26497 return null; 26498 } else { 26499 const { width, height, data } = image; 26500 const userImage = image; 26501 const isWebGLImage = isStyleImageWebGLData(userImage.data); 26502 return { 26503 data: isWebGLImage ? new RGBAImage({ 26504 width, 26505 height 26506 }) : new RGBAImage({ 26507 width, 26508 height 26509 }, new Uint8Array(data)), 26510 pixelRatio, 26511 stretchX, 26512 stretchY, 26513 content, 26514 textFitWidth, 26515 textFitHeight, 26516 sdf, 26517 version, 26518 isWebGLImage, 26519 userImage 26520 }; 26521 } 26522 } 26523 /** 26524 * Update an existing image in a style. This image can be displayed on the map like any other icon in the style's 26525 * sprite using the image's ID with 26526 * [`icon-image`](https://maplibre.org/maplibre-style-spec/layers/#layout-symbol-icon-image), 26527 * [`background-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-background-background-pattern), 26528 * [`fill-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-fill-fill-pattern), 26529 * or [`line-pattern`](https://maplibre.org/maplibre-style-spec/layers/#paint-line-line-pattern). 26530 * 26531 * An {@link ErrorEvent} will be fired if the image parameter is invalid. 26532 * 26533 * @param id - The ID of the image. 26534 * @param image - The image as an `HTMLImageElement`, `ImageData`, `ImageBitmap` or object with `width`, `height`, and `data` 26535 * properties with the same format as `ImageData`. 26536 * @example 26537 * ```ts 26538 * // If an image with the ID 'cat' already exists in the style's sprite, 26539 * // replace that image with a new image, 'other-cat-icon.png'. 26540 * if (map.hasImage('cat')) map.updateImage('cat', './other-cat-icon.png'); 26541 * ``` 26542 */ 26543 updateImage(id, image) { 26544 const existingImage = this.style.getImage(id); 26545 if (!existingImage) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead."))); 26546 const { width, height, data } = image instanceof HTMLImageElement || isImageBitmap(image) ? browser.getImageData(image) : image; 26547 if (width === void 0 || height === void 0) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`"))); 26548 if (width !== existingImage.data.width || height !== existingImage.data.height) return this.fire(new ErrorEvent(/* @__PURE__ */ new Error("The width and height of the updated image must be that same as the previous version of the image"))); 26549 existingImage.isWebGLImage = isStyleImageWebGLData(data); 26550 if (existingImage.isWebGLImage) existingImage.userImage = image; 26551 else { 26552 const copy = !(image instanceof HTMLImageElement || isImageBitmap(image)); 26553 existingImage.data.replace(data, copy); 26554 } 26555 this.style.updateImage(id, existingImage); 26556 return this; 26557 } 26558 /** 26559 * Returns an image, specified by ID, currently available in the map. 26560 * This includes both images from the style's original sprite 26561 * and any images that have been added at runtime using {@link Map.addImage}. 26562 * 26563 * @param id - The ID of the image. 26564 * @returns An image in the map with the specified ID. 26565 * 26566 * @example 26567 * ```ts 26568 * let coffeeShopIcon = map.getImage("coffee_cup"); 26569 * ``` 26570 */ 26571 getImage(id) { 26572 return this.style.getImage(id); 26573 } 26574 /** 26575 * Check whether or not an image with a specific ID exists in the style. This checks both images 26576 * in the style's original sprite and any images 26577 * that have been added at runtime using {@link Map.addImage}. 26578 * 26579 * An {@link ErrorEvent} will be fired if the image ID is missing. 26580 * 26581 * @param id - The ID of the image. 26582 * 26583 * @returns A Boolean indicating whether the image exists. 26584 * @example 26585 * Check if an image with the ID 'cat' exists in the style's sprite. 26586 * ```ts 26587 * let catIconExists = map.hasImage('cat'); 26588 * ``` 26589 */ 26590 hasImage(id) { 26591 if (!id) { 26592 this.fire(new ErrorEvent(/* @__PURE__ */ new Error("Missing required image id"))); 26593 return false; 26594 } 26595 return !!this.style.getImage(id); 26596 } 26597 /** 26598 * Remove an image from a style. This can be an image from the style's original 26599 * sprite or any images 26600 * that have been added at runtime using {@link Map.addImage}. 26601 * 26602 * @param id - The ID of the image. 26603 * 26604 * @example 26605 * ```ts 26606 * // If an image with the ID 'cat' exists in 26607 * // the style's sprite, remove it. 26608 * if (map.hasImage('cat')) map.removeImage('cat'); 26609 * ``` 26610 */ 26611 removeImage(id) { 26612 this.style.removeImage(id); 26613 } 26614 /** 26615 * Load an image from an external URL to be used with {@link Map.addImage}. External 26616 * domains must support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). 26617 * 26618 * @param url - The URL of the image file. Image file must be in png, webp, or jpg format. 26619 * @returns a promise that is resolved when the image is loaded, or rejected when the response has no image data (for example an HTTP 204) 26620 * 26621 * @example 26622 * Load an image from an external URL. 26623 * ```ts 26624 * const response = await map.loadImage('https://picsum.photos/50/50'); 26625 * // Add the loaded image to the style's sprite with the ID 'photo'. 26626 * map.addImage('photo', response.data); 26627 * ``` 26628 * @see [Add an icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-an-icon-to-the-map/) 26629 */ 26630 async loadImage(url) { 26631 const response = await ImageRequest.getImage(await this._requestManager.transformRequest(url, "Image"), new AbortController()); 26632 if (!response.data) throw new Error(`Could not load image ${url}: the response is empty`); 26633 return response; 26634 } 26635 /** 26636 * Returns an Array of strings containing the IDs of all images currently available in the map. 26637 * This includes both images from the style's original sprite 26638 * and any images that have been added at runtime using {@link Map.addImage}. 26639 * 26640 * @returns An Array of strings containing the names of all sprites/images currently available in the map. 26641 * 26642 * @example 26643 * ```ts 26644 * let allImages = map.listImages(); 26645 * ``` 26646 */ 26647 listImages() { 26648 return this.style?.listImages() ?? []; 26649 } 26650 /** 26651 * Adds a [MapLibre style layer](https://maplibre.org/maplibre-style-spec/layers) 26652 * to the map's style. 26653 * 26654 * A layer defines how data from a specified source will be styled. Read more about layer types 26655 * and available paint and layout properties in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/layers). 26656 * 26657 * @param layer - The layer to add, 26658 * conforming to either the MapLibre Style Specification's [layer definition](https://maplibre.org/maplibre-style-spec/layers) or, 26659 * less commonly, the {@link CustomLayerInterface} specification. Can also be a layer definition with an embedded source definition. 26660 * The MapLibre Style Specification's layer definition is appropriate for most layers. 26661 * 26662 * @param beforeId - The ID of an existing layer to insert the new layer before, 26663 * resulting in the new layer appearing visually beneath the existing layer. 26664 * If this argument is not specified, the layer will be appended to the end of the layers array 26665 * and appear visually above all other layers. 26666 * 26667 * @example 26668 * Add a circle layer with a vector source 26669 * ```ts 26670 * map.addLayer({ 26671 * id: 'points-of-interest', 26672 * source: { 26673 * type: 'vector', 26674 * url: 'https://demotiles.maplibre.org/tiles/tiles.json' 26675 * }, 26676 * 'source-layer': 'poi_label', 26677 * type: 'circle', 26678 * paint: { 26679 * // MapLibre Style Specification paint properties 26680 * }, 26681 * layout: { 26682 * // MapLibre Style Specification layout properties 26683 * } 26684 * }); 26685 * ``` 26686 * 26687 * @example 26688 * Define a source before using it to create a new layer 26689 * ```ts 26690 * map.addSource('state-data', { 26691 * type: 'geojson', 26692 * data: 'path/to/data.geojson' 26693 * }); 26694 * 26695 * map.addLayer({ 26696 * id: 'states', 26697 * // References the GeoJSON source defined above 26698 * // and does not require a `source-layer` 26699 * source: 'state-data', 26700 * type: 'symbol', 26701 * layout: { 26702 * // Set the label content to the 26703 * // feature's `name` property 26704 * text-field: ['get', 'name'] 26705 * } 26706 * }); 26707 * ``` 26708 * 26709 * @example 26710 * Add a new symbol layer before an existing layer 26711 * ```ts 26712 * map.addLayer({ 26713 * id: 'states', 26714 * // References a source that's already been defined 26715 * source: 'state-data', 26716 * type: 'symbol', 26717 * layout: { 26718 * // Set the label content to the 26719 * // feature's `name` property 26720 * text-field: ['get', 'name'] 26721 * } 26722 * // Add the layer before the existing `cities` layer 26723 * }, 'cities'); 26724 * ``` 26725 * @see [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/) 26726 * @see [Add a vector tile source](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-vector-tile-source/) 26727 * @see [Add a WMS source](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-wms-source/) 26728 */ 26729 addLayer(layer, beforeId) { 26730 this._lazyInitEmptyStyle(); 26731 this.style.addLayer(layer, beforeId); 26732 return this._update(true); 26733 } 26734 /** 26735 * Moves a layer to a different z-position. 26736 * 26737 * @param id - The ID of the layer to move. 26738 * @param beforeId - The ID of an existing layer to insert the new layer before. When viewing the map, the `id` layer will appear beneath the `beforeId` layer. If `beforeId` is omitted, the layer will be appended to the end of the layers array and appear above all other layers on the map. 26739 * 26740 * @example 26741 * Move a layer with ID 'polygon' before the layer with ID 'country-label'. The `polygon` layer will appear beneath the `country-label` layer on the map. 26742 * ```ts 26743 * map.moveLayer('polygon', 'country-label'); 26744 * ``` 26745 */ 26746 moveLayer(id, beforeId) { 26747 this.style.moveLayer(id, beforeId); 26748 return this._update(true); 26749 } 26750 /** 26751 * Removes the layer with the given ID from the map's style. 26752 * 26753 * An {@link ErrorEvent} will be fired if no such layer exists. 26754 * 26755 * @param id - The ID of the layer to remove 26756 * 26757 * @example 26758 * If a layer with ID 'state-data' exists, remove it. 26759 * ```ts 26760 * if (map.getLayer('state-data')) map.removeLayer('state-data'); 26761 * ``` 26762 */ 26763 removeLayer(id) { 26764 this.style.removeLayer(id); 26765 return this._update(true); 26766 } 26767 /** 26768 * Returns the layer with the specified ID in the map's style. 26769 * 26770 * @param id - The ID of the layer to get. 26771 * @returns The layer with the specified ID, or `undefined` 26772 * if the ID corresponds to no existing layers. 26773 * 26774 * @example 26775 * ```ts 26776 * let stateDataLayer = map.getLayer('state-data'); 26777 * ``` 26778 * @see [Filter symbols by toggling a list](https://maplibre.org/maplibre-gl-js/docs/examples/filter-symbols-by-toggling-a-list/) 26779 * @see [Filter symbols by text input](https://maplibre.org/maplibre-gl-js/docs/examples/filter-symbols-by-text-input/) 26780 */ 26781 getLayer(id) { 26782 return this.style?.getLayer(id); 26783 } 26784 /** 26785 * Return the ids of all layers currently in the style, including custom layers, in order. 26786 * 26787 * @returns ids of layers, in order 26788 * 26789 * @example 26790 * ```ts 26791 * const orderedLayerIds = map.getLayersOrder(); 26792 * ``` 26793 */ 26794 getLayersOrder() { 26795 return this.style?.getLayersOrder() ?? []; 26796 } 26797 /** 26798 * Sets the zoom extent for the specified style layer. The zoom extent includes the 26799 * [minimum zoom level](https://maplibre.org/maplibre-style-spec/layers/#minzoom) 26800 * and [maximum zoom level](https://maplibre.org/maplibre-style-spec/layers/#maxzoom)) 26801 * at which the layer will be rendered. 26802 * 26803 * !!! note 26804 * For style layers using vector sources, style layers cannot be rendered at zoom levels lower than the 26805 * minimum zoom level of the _source layer_ because the data does not exist at those zoom levels. If the minimum 26806 * zoom level of the source layer is higher than the minimum zoom level defined in the style layer, the style 26807 * layer will not be rendered at all zoom levels in the zoom range. 26808 * 26809 * @param layerId - The ID of the layer to which the zoom extent will be applied. 26810 * @param minzoom - The minimum zoom to set (0-24). 26811 * @param maxzoom - The maximum zoom to set (0-24). 26812 * 26813 * @example 26814 * ```ts 26815 * map.setLayerZoomRange('my-layer', 2, 5); 26816 * ``` 26817 */ 26818 setLayerZoomRange(layerId, minzoom, maxzoom) { 26819 this.style.setLayerZoomRange(layerId, minzoom, maxzoom); 26820 return this._update(true); 26821 } 26822 /** 26823 * Sets the filter for the specified style layer. 26824 * 26825 * Filters control which features a style layer renders from its source. 26826 * Any feature for which the filter expression evaluates to `true` will be 26827 * rendered on the map. Those that are false will be hidden. 26828 * 26829 * Use `setFilter` to show a subset of your source data. 26830 * 26831 * To clear the filter, pass `null` or `undefined` as the second parameter. 26832 * 26833 * @param layerId - The ID of the layer to which the filter will be applied. 26834 * @param filter - The filter, conforming to the MapLibre Style Specification's 26835 * [filter definition](https://maplibre.org/maplibre-style-spec/layers/#filter). If `null` or `undefined` is provided, the function removes any existing filter from the layer. 26836 * @param options - Options object. 26837 * 26838 * @example 26839 * Display only features with the 'name' property 'USA' 26840 * ```ts 26841 * map.setFilter('my-layer', ['==', ['get', 'name'], 'USA']); 26842 * ``` 26843 * @example 26844 * Display only features with five or more 'available-spots' 26845 * ```ts 26846 * map.setFilter('bike-docks', ['>=', ['get', 'available-spots'], 5]); 26847 * ``` 26848 * @example 26849 * Remove the filter for the 'bike-docks' style layer 26850 * ```ts 26851 * map.setFilter('bike-docks', null); 26852 * ``` 26853 * @see [Create a timeline animation](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-time-slider/) 26854 */ 26855 setFilter(layerId, filter, options = {}) { 26856 this.style?.setFilter(layerId, filter, options); 26857 return this._update(true); 26858 } 26859 /** 26860 * Returns the filter applied to the specified style layer. 26861 * 26862 * @param layerId - The ID of the style layer whose filter to get. 26863 * @returns The layer's filter. 26864 */ 26865 getFilter(layerId) { 26866 return this.style.getFilter(layerId); 26867 } 26868 /** 26869 * Sets the value of a paint property in the specified style layer. 26870 * 26871 * @param layerId - The ID of the layer to set the paint property in. 26872 * @param name - The name of the paint property to set. 26873 * @param value - The value of the paint property to set. 26874 * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/). 26875 * Pass `null` to unset the existing value. 26876 * @param options - Options object. 26877 * @example 26878 * ```ts 26879 * map.setPaintProperty('my-layer', 'fill-color', '#faafee'); 26880 * ``` 26881 * @see [Change a layer's color with buttons](https://maplibre.org/maplibre-gl-js/docs/examples/change-a-layers-color-with-buttons/) 26882 * @see [Create a draggable point](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-point/) 26883 */ 26884 setPaintProperty(layerId, name, value, options = {}) { 26885 this.style?.setPaintProperty(layerId, name, value, options); 26886 return this._update(true); 26887 } 26888 /** 26889 * Returns the value of a paint property in the specified style layer. 26890 * 26891 * @param layerId - The ID of the layer to get the paint property from. 26892 * @param name - The name of a paint property to get. 26893 * @returns The value of the specified paint property. 26894 */ 26895 getPaintProperty(layerId, name) { 26896 return this.style.getPaintProperty(layerId, name); 26897 } 26898 /** 26899 * Sets the value of a layout property in the specified style layer. 26900 * 26901 * @param layerId - The ID of the layer to set the layout property in. 26902 * @param name - The name of the layout property to set. 26903 * @param value - The value of the layout property. Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/). 26904 * @param options - The options object. 26905 * @example 26906 * ```ts 26907 * map.setLayoutProperty('my-layer', 'visibility', 'none'); 26908 * ``` 26909 */ 26910 setLayoutProperty(layerId, name, value, options = {}) { 26911 this.style.setLayoutProperty(layerId, name, value, options); 26912 return this._update(true); 26913 } 26914 /** 26915 * Returns the value of a layout property in the specified style layer. 26916 * 26917 * @param layerId - The ID of the layer to get the layout property from. 26918 * @param name - The name of the layout property to get. 26919 * @returns The value of the specified layout property. 26920 */ 26921 getLayoutProperty(layerId, name) { 26922 return this.style.getLayoutProperty(layerId, name); 26923 } 26924 /** 26925 * Sets the value of the style's glyphs property. Pass a falsy value (null or undefined) 26926 * to unset glyphs. 26927 * 26928 * @param glyphsUrl - Glyph URL to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/glyphs/). 26929 * @param options - Options object. 26930 * @example 26931 * ```ts 26932 * map.setGlyphs('https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf'); 26933 * ``` 26934 */ 26935 setGlyphs(glyphsUrl, options = {}) { 26936 this._lazyInitEmptyStyle(); 26937 this.style.setGlyphs(glyphsUrl, options); 26938 return this._update(true); 26939 } 26940 /** 26941 * Returns the value of the style's glyphs URL 26942 * 26943 * @returns glyphs Style's glyphs url, or `null` if glyphs are unset. 26944 */ 26945 getGlyphs() { 26946 return this.style.getGlyphsUrl(); 26947 } 26948 /** 26949 * Sets the value of the style's `font-faces` property, which points at the font files used to 26950 * draw text that the style's `glyphs` URL does not cover. Pass a falsy value (null or undefined) 26951 * to unset it. 26952 * 26953 * The files are handed to the browser's CSS Font Loading API, so any format the browser can 26954 * render text with may be used, and requests for them go through `transformRequest` as glyph 26955 * requests do. Text is drawn a grapheme cluster at a time, so a letter and the marks written on 26956 * it are handed to the browser's text engine together and come back as the one shape they are 26957 * written as -- which is what a `glyphs` URL, serving one codepoint at a time, cannot do. 26958 * 26959 * @param fontFaces - The font faces to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/root/#font-faces). 26960 * A declaration this cannot make sense of is skipped with a warning, as is a font file that 26961 * fails to load, so the text it would have drawn falls back to the `glyphs` URL. 26962 * @example 26963 * ```ts 26964 * map.setFontFaces({ 26965 * 'Noto Sans Regular': [ 26966 * {url: 'https://example.com/NotoSansKhmer-Regular.ttf', 'unicode-range': ['U+1780-17FF']} 26967 * ] 26968 * }); 26969 * ``` 26970 */ 26971 setFontFaces(fontFaces) { 26972 this._lazyInitEmptyStyle(); 26973 this.style.setFontFaces(fontFaces); 26974 return this._update(true); 26975 } 26976 /** 26977 * Returns the value of the style's `font-faces` property. 26978 * 26979 * @returns The style's font faces, or `null` if it declares none. 26980 */ 26981 getFontFaces() { 26982 return this.style.getFontFaces(); 26983 } 26984 /** 26985 * Adds a sprite to the map's style. Fires the `style` event. 26986 * 26987 * @param id - The ID of the sprite to add. Must not conflict with existing sprites. 26988 * @param url - The URL to load the sprite from 26989 * @param options - Options object. 26990 * @example 26991 * ```ts 26992 * map.addSprite('sprite-two', 'http://example.com/sprite-two'); 26993 * ``` 26994 */ 26995 addSprite(id, url, options = {}) { 26996 this._lazyInitEmptyStyle(); 26997 this.style.addSprite(id, url, options, (err) => { 26998 if (!err) this._update(true); 26999 }); 27000 return this; 27001 } 27002 /** 27003 * Removes the sprite from the map's style. Fires the `style` event. 27004 * 27005 * @param id - The ID of the sprite to remove. If the sprite is declared as a single URL, the ID must be "default". 27006 * @example 27007 * ```ts 27008 * map.removeSprite('sprite-two'); 27009 * map.removeSprite('default'); 27010 * ``` 27011 */ 27012 removeSprite(id) { 27013 this._lazyInitEmptyStyle(); 27014 this.style.removeSprite(id); 27015 return this._update(true); 27016 } 27017 /** 27018 * Returns the as-is value of the style's sprite. 27019 * 27020 * @returns style's sprite list of id-url pairs 27021 */ 27022 getSprite() { 27023 return this.style.getSprite(); 27024 } 27025 /** 27026 * Sets the value of the style's sprite property. 27027 * 27028 * @param spriteUrl - Sprite URL to set. 27029 * @param options - Options object. 27030 * @example 27031 * ```ts 27032 * map.setSprite('YOUR_SPRITE_URL'); 27033 * ``` 27034 */ 27035 setSprite(spriteUrl, options = {}) { 27036 this._lazyInitEmptyStyle(); 27037 this.style.setSprite(spriteUrl, options, (err) => { 27038 if (!err) this._update(true); 27039 }); 27040 return this; 27041 } 27042 /** 27043 * Sets the any combination of light values. 27044 * 27045 * @param light - Light properties to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/light). 27046 * @param options - Options object. 27047 * 27048 * @example 27049 * ```ts 27050 * let layerVisibility = map.getLayoutProperty('my-layer', 'visibility'); 27051 * ``` 27052 */ 27053 setLight(light, options = {}) { 27054 this._lazyInitEmptyStyle(); 27055 this.style.setLight(light, options); 27056 return this._update(true); 27057 } 27058 /** 27059 * Returns the value of the light object. 27060 * 27061 * @returns light Light properties of the style. 27062 */ 27063 getLight() { 27064 return this.style.getLight(); 27065 } 27066 /** 27067 * Sets the value of style's sky properties. 27068 * 27069 * @param sky - Sky properties to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/sky/). 27070 * @param options - Options object. 27071 * 27072 * @example 27073 * ```ts 27074 * map.setSky({'atmosphere-blend': 1.0}); 27075 * ``` 27076 */ 27077 setSky(sky, options = {}) { 27078 this._lazyInitEmptyStyle(); 27079 this.style.setSky(sky, options); 27080 return this._update(true); 27081 } 27082 /** 27083 * Returns the value of the style's sky. 27084 * 27085 * @returns the sky properties of the style. 27086 * @example 27087 * ```ts 27088 * map.getSky(); 27089 * ``` 27090 */ 27091 getSky() { 27092 return this.style.getSky(); 27093 } 27094 /** 27095 * Sets the `state` of a feature. 27096 * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. 27097 * When using this method, the `state` object is merged with any existing key-value pairs in the feature's state. 27098 * Features are identified by their `feature.id` attribute, which can be any number or string. 27099 * 27100 * This method can only be used with sources that have a `feature.id` attribute. The `feature.id` attribute can be defined in three ways: 27101 * 27102 * - For vector or GeoJSON sources, including an `id` attribute in the original data file. 27103 * - For vector or GeoJSON sources, using the [`promoteId`](https://maplibre.org/maplibre-style-spec/sources/#promoteid) option at the time the source is defined. 27104 * - For GeoJSON sources, using the [`generateId`](https://maplibre.org/maplibre-style-spec/sources/#generateid) option to auto-assign an `id` based on the feature's index in the source data. If you change feature data using `map.getSource('some id').setData(..)`, you may need to re-apply state taking into account updated `id` values. 27105 * 27106 * !!! note 27107 * You can use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) to access the values in a feature's state object for the purposes of styling. 27108 * 27109 * @param feature - Feature identifier. Feature objects returned from 27110 * {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers. 27111 * @param state - A set of key-value pairs. The values should be valid JSON types. 27112 * 27113 * @example 27114 * ```ts 27115 * // When the mouse moves over the `my-layer` layer, update 27116 * // the feature state for the feature under the mouse 27117 * map.on('mousemove', 'my-layer', (e) => { 27118 * if (e.features.length > 0) { 27119 * map.setFeatureState({ 27120 * source: 'my-source', 27121 * sourceLayer: 'my-source-layer', 27122 * id: e.features[0].id, 27123 * }, { 27124 * hover: true 27125 * }); 27126 * } 27127 * }); 27128 * ``` 27129 * @see [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-hover-effect/) 27130 */ 27131 setFeatureState(feature, state) { 27132 this.style.setFeatureState(feature, state); 27133 return this._update(); 27134 } 27135 /** 27136 * Removes the `state` of a feature, setting it back to the default behavior. 27137 * If only a `target.source` is specified, it will remove the state for all features from that source. 27138 * If `target.id` is also specified, it will remove all keys for that feature's state. 27139 * If `key` is also specified, it removes only that key from that feature's state. 27140 * Features are identified by their `feature.id` attribute, which can be any number or string. 27141 * 27142 * @param target - Identifier of where to remove state. It can be a source, a feature, or a specific key of feature. 27143 * Feature objects returned from {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers. 27144 * @param key - (optional) The key in the feature state to reset. 27145 * @example 27146 * Reset the entire state object for all features in the `my-source` source 27147 * ```ts 27148 * map.removeFeatureState({ 27149 * source: 'my-source' 27150 * }); 27151 * ``` 27152 * 27153 * @example 27154 * When the mouse leaves the `my-layer` layer, 27155 * reset the entire state object for the 27156 * feature under the mouse 27157 * ```ts 27158 * map.on('mouseleave', 'my-layer', (e) => { 27159 * map.removeFeatureState({ 27160 * source: 'my-source', 27161 * sourceLayer: 'my-source-layer', 27162 * id: e.features[0].id 27163 * }); 27164 * }); 27165 * ``` 27166 * 27167 * @example 27168 * When the mouse leaves the `my-layer` layer, 27169 * reset only the `hover` key-value pair in the 27170 * state for the feature under the mouse 27171 * ```ts 27172 * map.on('mouseleave', 'my-layer', (e) => { 27173 * map.removeFeatureState({ 27174 * source: 'my-source', 27175 * sourceLayer: 'my-source-layer', 27176 * id: e.features[0].id 27177 * }, 'hover'); 27178 * }); 27179 * ``` 27180 */ 27181 removeFeatureState(target, key) { 27182 this.style.removeFeatureState(target, key); 27183 return this._update(); 27184 } 27185 /** 27186 * Gets the `state` of a feature. 27187 * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. 27188 * Features are identified by their `feature.id` attribute, which can be any number or string. 27189 * 27190 * !!! note 27191 * To access the values in a feature's state object for the purposes of styling the feature, use the [`feature-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#feature-state). 27192 * 27193 * @param feature - Feature identifier. Feature objects returned from 27194 * {@link Map.queryRenderedFeatures} or event handlers can be used as feature identifiers. 27195 * @returns The state of the feature: a set of key-value pairs that was assigned to the feature at runtime. 27196 * 27197 * @example 27198 * When the mouse moves over the `my-layer` layer, 27199 * get the feature state for the feature under the mouse 27200 * ```ts 27201 * map.on('mousemove', 'my-layer', (e) => { 27202 * if (e.features.length > 0) { 27203 * map.getFeatureState({ 27204 * source: 'my-source', 27205 * sourceLayer: 'my-source-layer', 27206 * id: e.features[0].id 27207 * }); 27208 * } 27209 * }); 27210 * ``` 27211 */ 27212 getFeatureState(feature) { 27213 return this.style.getFeatureState(feature); 27214 } 27215 /** 27216 * Returns the map's containing HTML element. 27217 * 27218 * @returns The map's container. 27219 */ 27220 getContainer() { 27221 return this._container; 27222 } 27223 /** 27224 * Returns the HTML element containing the map's `<canvas>` element. 27225 * 27226 * If you want to add non-GL overlays to the map, you should append them to this element. 27227 * 27228 * This is the element to which event bindings for map interactivity (such as panning and zooming) are 27229 * attached. It will receive bubbled events from child elements such as the `<canvas>`, but not from 27230 * map controls. 27231 * 27232 * @returns The container of the map's `<canvas>`. 27233 * @see [Create a draggable point](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-point/) 27234 */ 27235 getCanvasContainer() { 27236 return this._canvasContainer; 27237 } 27238 /** 27239 * Returns the map's `<canvas>` element. 27240 * 27241 * @returns The map's `<canvas>` element. 27242 * @see [Measure distances](https://maplibre.org/maplibre-gl-js/docs/examples/measure-distances/) 27243 * @see [Display a popup on hover](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-hover/) 27244 * @see [Center the map on a clicked symbol](https://maplibre.org/maplibre-gl-js/docs/examples/center-the-map-on-a-clicked-symbol/) 27245 */ 27246 getCanvas() { 27247 return this._canvas; 27248 } 27249 _containerDimensions() { 27250 let width = 0; 27251 let height = 0; 27252 if (this._container) { 27253 width = this._container.clientWidth || 400; 27254 height = this._container.clientHeight || 300; 27255 } 27256 return [width, height]; 27257 } 27258 /** 27259 * Determines if the initial resize event should be handled based on the container's dimensions. 27260 * 27261 * @returns `true` if the initial resize event should be handled, `false` otherwise. 27262 */ 27263 _shouldHandleInitialResize() { 27264 if (!this._container?.clientWidth || !this._container.clientHeight) return false; 27265 const [width, height] = this._containerDimensions(); 27266 return width !== this._camera.transform.width || height !== this._camera.transform.height; 27267 } 27268 /** 27269 * @internal 27270 * Sets up the ResizeObserver to track container size changes. 27271 * Uses the owning window's ResizeObserver for cross-window support. 27272 */ 27273 _setupResizeObserver() { 27274 let initialResizeEventCaptured = false; 27275 const throttledResizeCallback = throttle((entries) => { 27276 if (this._trackResize && !this._removed) { 27277 this.resize(entries); 27278 this.redraw(); 27279 } 27280 }, 50); 27281 const ResizeObserverClass = this._ownerWindow.ResizeObserver ?? ResizeObserver; 27282 this._resizeObserver = new ResizeObserverClass((entries) => { 27283 if (!initialResizeEventCaptured) { 27284 initialResizeEventCaptured = true; 27285 if (!this._shouldHandleInitialResize()) return; 27286 } 27287 throttledResizeCallback(entries); 27288 }); 27289 this._resizeObserver.observe(this._container); 27290 } 27291 /** 27292 * @internal 27293 * Resolves the container option to an HTMLElement. 27294 * Supports string ID, HTMLElement, or cross-window elements (using nodeType check). 27295 */ 27296 _resolveContainer(container) { 27297 if (typeof container === "string") { 27298 const element = document.getElementById(container); 27299 if (!element) throw new Error(`Container '${container}' not found.`); 27300 return element; 27301 } 27302 if (container instanceof HTMLElement) return container; 27303 if (container && typeof container === "object" && container.nodeType === 1) return container; 27304 throw new Error("Invalid type: 'container' must be a String or HTMLElement."); 27305 } 27306 _setupContainer() { 27307 const dimensions = this._containerDimensions(); 27308 const clampedPixelRatio = this._getClampedPixelRatio(dimensions[0], dimensions[1]); 27309 const container = this._container; 27310 container.classList.add("maplibregl-map"); 27311 const canvasContainer = this._canvasContainer = DOM.create("div", "maplibregl-canvas-container", container); 27312 if (this._interactive) canvasContainer.classList.add("maplibregl-interactive"); 27313 this._canvas = DOM.create("canvas", "maplibregl-canvas", canvasContainer); 27314 this._canvas.addEventListener("webglcontextlost", this._contextLost, false); 27315 this._canvas.addEventListener("webglcontextrestored", this._contextRestored, false); 27316 this._canvas.setAttribute("tabindex", this._interactive ? "0" : "-1"); 27317 this._canvas.setAttribute("aria-label", this._getUIString("Map.Title")); 27318 this._canvas.setAttribute("role", "region"); 27319 this._resizeCanvas(dimensions[0], dimensions[1], clampedPixelRatio); 27320 const controlContainer = this._controlContainer = DOM.create("div", "maplibregl-control-container", container); 27321 const positions = this._controlPositions = {}; 27322 for (const positionName of [ 27323 "top-left", 27324 "top-right", 27325 "bottom-left", 27326 "bottom-right" 27327 ]) positions[positionName] = DOM.create("div", `maplibregl-ctrl-${positionName} `, controlContainer); 27328 this._container.addEventListener("scroll", this._onMapScroll, false); 27329 } 27330 /** 27331 * @internal 27332 * Reverses the DOM mutations of {@link Map._setupContainer}, returning the container element to its pre-construction state. 27333 */ 27334 _cleanupContainer() { 27335 this._canvas.removeEventListener("webglcontextrestored", this._contextRestored, false); 27336 this._canvas.removeEventListener("webglcontextlost", this._contextLost, false); 27337 this._canvasContainer.remove(); 27338 this._controlContainer.remove(); 27339 this._container.removeEventListener("scroll", this._onMapScroll, false); 27340 this._container.classList.remove("maplibregl-map"); 27341 } 27342 _resizeCanvas(width, height, pixelRatio) { 27343 this._canvas.width = Math.floor(pixelRatio * width); 27344 this._canvas.height = Math.floor(pixelRatio * height); 27345 this._canvas.style.width = `${width}px`; 27346 this._canvas.style.height = `${height}px`; 27347 } 27348 /** 27349 * @internal 27350 * Creates the WebGL2 context and the painter. Throws {@link GPUInitializationError} when the context cannot be created. 27351 */ 27352 _setupPainter() { 27353 const attributes = { 27354 ...this._canvasContextAttributes, 27355 alpha: true, 27356 depth: true, 27357 stencil: true, 27358 premultipliedAlpha: true 27359 }; 27360 let creationEvent = null; 27361 this._canvas.addEventListener("webglcontextcreationerror", (event) => { 27362 creationEvent = event; 27363 }, { once: true }); 27364 const gl = this._canvas.getContext("webgl2", attributes); 27365 if (!gl) throw new GPUInitializationError(attributes, creationEvent); 27366 this.painter = new Painter(gl, this._camera.transform); 27367 } 27368 /** 27369 * @internal 27370 * Creates a new specialized transform instance from a projection instance and migrates 27371 * to this new transform, carrying over all the properties of the old transform (center, pitch, etc.). 27372 * When the style's projection is changed (or first set), this function should be called. 27373 */ 27374 migrateProjection(newTransform, newCameraHelper) { 27375 this._camera.migrateProjection(newTransform, newCameraHelper); 27376 this.painter.transform = newTransform; 27377 this.fire(new MapProjectionEvent({ newProjection: this.style.projection.name })); 27378 } 27379 /** 27380 * Returns a Boolean indicating whether the map is fully loaded. 27381 * 27382 * Returns `false` if the style is not yet fully loaded, 27383 * or if there has been a change to the sources or style that 27384 * has not yet fully loaded. 27385 * 27386 * @returns A Boolean indicating whether the map is fully loaded. 27387 */ 27388 loaded() { 27389 return !this._styleDirty && !this._sourcesDirty && !!this.style && this.style.loaded(); 27390 } 27391 /** 27392 * @internal 27393 * Update this map's style and sources, and re-render the map. 27394 * 27395 * @param updateStyle - mark the map's style for reprocessing as 27396 * well as its sources 27397 */ 27398 _update(updateStyle) { 27399 if (!this.style?._loaded) return this; 27400 this._styleDirty ||= updateStyle; 27401 this._sourcesDirty = true; 27402 this.triggerRepaint(); 27403 return this; 27404 } 27405 /** 27406 * @internal 27407 * Request that the given callback be executed during the next render 27408 * frame. Schedule a render frame if one is not already scheduled. 27409 * 27410 * @returns An id that can be used to cancel the callback 27411 */ 27412 _requestRenderFrame(callback) { 27413 this._update(); 27414 return this._renderTaskQueue.add(callback); 27415 } 27416 _cancelRenderFrame(id) { 27417 this._renderTaskQueue.remove(id); 27418 } 27419 /** 27420 * @internal 27421 * Call when a (re-)render of the map is required: 27422 * 27423 * - The style has changed (`setPaintProperty()`, etc.) 27424 * - Source data has changed (e.g. tiles have finished loading) 27425 * - The map has is moving (or just finished moving) 27426 * - A transition is in progress 27427 * 27428 * @param paintStartTimeStamp - The time when the animation frame began executing. 27429 */ 27430 _render(paintStartTimeStamp) { 27431 const fadeDuration = this._idleTriggered ? this._fadeDuration : 0; 27432 const isGlobeRendering = this.style.projection?.transitionState > 0; 27433 this.painter.context.setDirty(); 27434 this.painter.setBaseState(); 27435 this._renderTaskQueue.run(paintStartTimeStamp); 27436 if (this._removed) return; 27437 let crossFading = false; 27438 if (this.style && this._styleDirty) { 27439 this._styleDirty = false; 27440 const zoom = this._camera.transform.zoom; 27441 const currentTime = now(); 27442 this.style.zoomHistory.update(zoom, currentTime); 27443 const parameters = new EvaluationParameters(zoom, { 27444 now: currentTime, 27445 fadeDuration, 27446 zoomHistory: this.style.zoomHistory, 27447 transition: this.style.getTransition() 27448 }); 27449 const factor = parameters.crossFadingFactor(); 27450 if (factor !== 1 || factor !== this._crossFadingFactor) { 27451 crossFading = true; 27452 this._crossFadingFactor = factor; 27453 } 27454 this.style.update(parameters); 27455 } 27456 const globeRenderingChanged = this.style.projection?.transitionState > 0 !== isGlobeRendering; 27457 this._camera.transform.setTransitionState(this.style.projection?.transitionState); 27458 if (this.style && (this._sourcesDirty || globeRenderingChanged)) { 27459 this._sourcesDirty = false; 27460 this.style._updateSources(this._camera.transform); 27461 } 27462 if (this.terrain) { 27463 if (this.terrain.tileManager.update(this._camera.transform, this.terrain)) this.terrain.resetElevationCache(); 27464 this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center, this._camera.transform.tileZoom)); 27465 if (!this._camera.elevationFreeze && this.getCenterClampedToGround()) this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center, this._camera.transform)); 27466 } else { 27467 this._camera.transform.setMinElevationForCurrentTile(0); 27468 if (this.getCenterClampedToGround()) this._camera.transform.setElevation(0); 27469 } 27470 this._placementDirty = this.style?._updatePlacement(this._camera.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, globeRenderingChanged); 27471 this.painter.render(this.style, { 27472 showTileBoundaries: this.showTileBoundaries, 27473 showOverdrawInspector: this._showOverdrawInspector, 27474 rotating: this.isRotating(), 27475 zooming: this.isZooming(), 27476 moving: this.isMoving(), 27477 fadeDuration, 27478 showPadding: this.showPadding, 27479 anisotropicFilterPitch: this.getAnisotropicFilterPitch() 27480 }); 27481 this.fire(new MapLibreEvent("render")); 27482 if (this.loaded() && !this._loaded) { 27483 this._loaded = true; 27484 this.fire(new MapLibreEvent("load")); 27485 } 27486 if (this.style && (this.style.hasTransitions() || crossFading)) this._styleDirty = true; 27487 if (this.style && !this._placementDirty) this.style._releaseSymbolFadeTiles(); 27488 const somethingDirty = this._sourcesDirty || this._styleDirty || this._placementDirty || this.painter.renderToTexture?.needsFollowUpFrame; 27489 if (somethingDirty || this._repaint) this.triggerRepaint(); 27490 else if (!this.isMoving() && this.loaded()) this.fire(new MapLibreEvent("idle")); 27491 if (this._loaded && !this._fullyLoaded && !somethingDirty) this._fullyLoaded = true; 27492 return this; 27493 } 27494 /** 27495 * Force a synchronous redraw of the map. 27496 * @example 27497 * ```ts 27498 * map.redraw(); 27499 * ``` 27500 */ 27501 redraw() { 27502 if (this.style) { 27503 if (this._frameRequest) { 27504 this._frameRequest.abort(); 27505 this._frameRequest = null; 27506 } 27507 this._render(0); 27508 } 27509 return this; 27510 } 27511 /** 27512 * Clean up and release all internal resources associated with this map. 27513 * 27514 * This includes DOM elements, event bindings, web workers, and WebGL resources. 27515 * 27516 * Use this method when you are done using the map and wish to ensure that it no 27517 * longer consumes browser resources. Afterwards, you must not call any other 27518 * methods on the map. 27519 */ 27520 remove() { 27521 if (this._hash) this._hash.remove(); 27522 for (const control of this._controls) control.onRemove(this); 27523 this._controls = []; 27524 if (this._frameRequest) { 27525 this._frameRequest.abort(); 27526 this._frameRequest = null; 27527 } 27528 this._renderTaskQueue.clear(); 27529 this._diffStyleRequest?.abort(); 27530 this.painter.destroy(); 27531 this._handlers.destroy(); 27532 this.setStyle(null); 27533 if (typeof window !== "undefined") this._ownerWindow.removeEventListener("online", this._onWindowOnline, false); 27534 ImageRequest.removeThrottleControl(this._imageQueueHandle); 27535 this._resizeObserver?.disconnect(); 27536 const extension = this.painter.context.gl.getExtension("WEBGL_lose_context"); 27537 if (extension?.loseContext) extension.loseContext(); 27538 this._cleanupContainer(); 27539 this._removed = true; 27540 this.fire(new MapLibreEvent("remove")); 27541 } 27542 /** 27543 * Trigger the rendering of a single frame. Use this method with custom layers to 27544 * repaint the map when the layer changes. Calling this multiple times before the 27545 * next frame is rendered will still result in only a single frame being rendered. 27546 * @example 27547 * ```ts 27548 * map.triggerRepaint(); 27549 * ``` 27550 * @see [Add a 3D model](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-3d-model-using-threejs/) 27551 * @see [Add an animated icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-an-animated-icon-to-the-map/) 27552 */ 27553 triggerRepaint() { 27554 if (this.style && !this._frameRequest) { 27555 this._frameRequest = new AbortController(); 27556 browser.frame(this._frameRequest, (paintStartTimeStamp) => { 27557 this._frameRequest = null; 27558 try { 27559 this._render(paintStartTimeStamp); 27560 } catch (error) { 27561 if (!isAbortError(error)) throw error; 27562 } 27563 }, () => {}, this._ownerWindow); 27564 } 27565 } 27566 /** 27567 * Gets and sets a Boolean indicating whether the map will render an outline 27568 * around each tile and the tile ID. These tile boundaries are useful for 27569 * debugging. 27570 * 27571 * The uncompressed file size of the first vector source is drawn in the top left 27572 * corner of each tile, next to the tile ID. 27573 * 27574 * @example 27575 * ```ts 27576 * map.showTileBoundaries = true; 27577 * ``` 27578 */ 27579 get showTileBoundaries() { 27580 return !!this._showTileBoundaries; 27581 } 27582 set showTileBoundaries(value) { 27583 if (this._showTileBoundaries === value) return; 27584 this._showTileBoundaries = value; 27585 this._update(); 27586 } 27587 /** 27588 * Gets and sets a Boolean indicating whether the map will visualize 27589 * the padding offsets. 27590 */ 27591 get showPadding() { 27592 return !!this._showPadding; 27593 } 27594 set showPadding(value) { 27595 if (this._showPadding === value) return; 27596 this._showPadding = value; 27597 this._update(); 27598 } 27599 /** 27600 * Gets and sets a Boolean indicating whether the map will render boxes 27601 * around all symbols in the data source, revealing which symbols 27602 * were rendered or which were hidden due to collisions. 27603 * This information is useful for debugging. 27604 */ 27605 get showCollisionBoxes() { 27606 return !!this._showCollisionBoxes; 27607 } 27608 set showCollisionBoxes(value) { 27609 if (this._showCollisionBoxes === value) return; 27610 this._showCollisionBoxes = value; 27611 if (value) this.style._generateCollisionBoxes(); 27612 else this._update(); 27613 } 27614 /** 27615 * Gets and sets a Boolean indicating whether the map should color-code 27616 * each fragment to show how many times it has been shaded. 27617 * White fragments have been shaded 8 or more times. 27618 * Black fragments have been shaded 0 times. 27619 * This information is useful for debugging. 27620 */ 27621 get showOverdrawInspector() { 27622 return !!this._showOverdrawInspector; 27623 } 27624 set showOverdrawInspector(value) { 27625 if (this._showOverdrawInspector === value) return; 27626 this._showOverdrawInspector = value; 27627 this._update(); 27628 } 27629 /** 27630 * Gets and sets a Boolean indicating whether the map will 27631 * continuously repaint. This information is useful for analyzing performance. 27632 */ 27633 get repaint() { 27634 return !!this._repaint; 27635 } 27636 set repaint(value) { 27637 if (this._repaint !== value) { 27638 this._repaint = value; 27639 this.triggerRepaint(); 27640 } 27641 } 27642 get vertices() { 27643 return !!this._vertices; 27644 } 27645 set vertices(value) { 27646 this._vertices = value; 27647 this._update(); 27648 } 27649 /** 27650 * Returns the package version of the library 27651 * @returns Package version of the library 27652 */ 27653 get version() { 27654 return version$1; 27655 } 27656 /** 27657 * Returns the elevation for the point where the camera is looking. 27658 * This value corresponds to: 27659 * "meters above sea level" * "exaggeration" 27660 * @returns The elevation. 27661 */ 27662 getCameraTargetElevation() { 27663 return this._camera.transform.elevation; 27664 } 27665 /** 27666 * Gets the {@link ProjectionSpecification}. 27667 * @returns the projection specification. 27668 * @example 27669 * ```ts 27670 * let projection = map.getProjection(); 27671 * ``` 27672 */ 27673 getProjection() { 27674 return this.style.getProjection(); 27675 } 27676 /** 27677 * Sets the {@link ProjectionSpecification}. 27678 * @param projection - the projection specification to set 27679 * @returns 27680 */ 27681 setProjection(projection) { 27682 this._lazyInitEmptyStyle(); 27683 this.style.setProjection(projection); 27684 return this._update(true); 27685 } 27686}; 27687//#endregion 27688//#region src/ui/control/navigation_control.ts 27689const defaultOptions$3 = { 27690 showCompass: true, 27691 showZoom: true, 27692 visualizePitch: false, 27693 visualizeRoll: true 27694}; 27695/** 27696* A `NavigationControl` control contains zoom buttons and a compass. 27697* 27698* @group Markers and Controls 27699* 27700* @example 27701* ```ts 27702* let nav = new NavigationControl(); 27703* map.addControl(nav, 'top-left'); 27704* ``` 27705* @see [Display map navigation controls](https://maplibre.org/maplibre-gl-js/docs/examples/display-map-navigation-controls/) 27706*/ 27707var NavigationControl = class { 27708 /** 27709 * @param options - the control's options 27710 */ 27711 constructor(options) { 27712 this._updateZoomButtons = () => { 27713 const zoom = this._map.getZoom(); 27714 const isMax = zoom === this._map.getMaxZoom(); 27715 const isMin = zoom === this._map.getMinZoom(true); 27716 this._zoomInButton.disabled = isMax; 27717 this._zoomOutButton.disabled = isMin; 27718 this._zoomInButton.setAttribute("aria-disabled", isMax.toString()); 27719 this._zoomOutButton.setAttribute("aria-disabled", isMin.toString()); 27720 }; 27721 this._rotateCompassArrow = () => { 27722 const pitch = this._map.getPitch(); 27723 const roll = this._map.getRoll(); 27724 const bearing = this._map.getBearing(); 27725 const pitchScale = 1 / Math.pow(Math.cos(degreesToRadians(pitch)), .5); 27726 if (this.options.visualizePitch && this.options.visualizeRoll) { 27727 this._compassIcon.style.transform = `scale(${pitchScale}) rotateZ(${-roll}deg) rotateX(${pitch}deg) rotateZ(${-bearing}deg)`; 27728 return; 27729 } 27730 if (this.options.visualizePitch) { 27731 this._compassIcon.style.transform = `scale(${pitchScale}) rotateX(${pitch}deg) rotateZ(${-bearing}deg)`; 27732 return; 27733 } 27734 if (this.options.visualizeRoll) { 27735 this._compassIcon.style.transform = `rotate(${-bearing - roll}deg)`; 27736 return; 27737 } 27738 this._compassIcon.style.transform = `rotate(${-bearing}deg)`; 27739 }; 27740 this._setButtonTitle = (button, title) => { 27741 const str = this._map._getUIString(`NavigationControl.${title}`); 27742 button.title = str; 27743 button.setAttribute("aria-label", str); 27744 }; 27745 this.options = extend({}, defaultOptions$3, options); 27746 this._container = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-group"); 27747 this._container.addEventListener("contextmenu", (e) => e.preventDefault()); 27748 if (this.options.showZoom) { 27749 this._zoomInButton = this._createButton("maplibregl-ctrl-zoom-in", (e) => this._map.zoomIn({}, { originalEvent: e })); 27750 DOM.create("span", "maplibregl-ctrl-icon", this._zoomInButton).setAttribute("aria-hidden", "true"); 27751 this._zoomOutButton = this._createButton("maplibregl-ctrl-zoom-out", (e) => this._map.zoomOut({}, { originalEvent: e })); 27752 DOM.create("span", "maplibregl-ctrl-icon", this._zoomOutButton).setAttribute("aria-hidden", "true"); 27753 } 27754 if (this.options.showCompass) { 27755 this._compass = this._createButton("maplibregl-ctrl-compass", (e) => { 27756 if (this.options.visualizePitch) this._map.resetNorthPitch({}, { originalEvent: e }); 27757 else this._map.resetNorth({}, { originalEvent: e }); 27758 }); 27759 this._compassIcon = DOM.create("span", "maplibregl-ctrl-icon", this._compass); 27760 this._compassIcon.setAttribute("aria-hidden", "true"); 27761 } 27762 } 27763 /** {@inheritDoc IControl.onAdd} */ 27764 onAdd(map) { 27765 this._map = map; 27766 if (this.options.showZoom) { 27767 this._setButtonTitle(this._zoomInButton, "ZoomIn"); 27768 this._setButtonTitle(this._zoomOutButton, "ZoomOut"); 27769 this._map.on("move", this._updateZoomButtons); 27770 this._updateZoomButtons(); 27771 } 27772 if (this.options.showCompass) { 27773 this._setButtonTitle(this._compass, "ResetBearing"); 27774 if (this.options.visualizePitch) this._map.on("pitch", this._rotateCompassArrow); 27775 if (this.options.visualizeRoll) this._map.on("roll", this._rotateCompassArrow); 27776 this._map.on("rotate", this._rotateCompassArrow); 27777 this._rotateCompassArrow(); 27778 this._handler = new MouseRotateWrapper(this._map, this._compass, this.options.visualizePitch); 27779 } 27780 return this._container; 27781 } 27782 /** {@inheritDoc IControl.onRemove} */ 27783 onRemove() { 27784 this._container.remove(); 27785 if (this.options.showZoom) this._map.off("move", this._updateZoomButtons); 27786 if (this.options.showCompass) { 27787 if (this.options.visualizePitch) this._map.off("pitch", this._rotateCompassArrow); 27788 if (this.options.visualizeRoll) this._map.off("roll", this._rotateCompassArrow); 27789 this._map.off("rotate", this._rotateCompassArrow); 27790 this._handler.off(); 27791 delete this._handler; 27792 } 27793 delete this._map; 27794 } 27795 _createButton(className, fn) { 27796 const a = DOM.create("button", className, this._container); 27797 a.type = "button"; 27798 a.addEventListener("click", fn); 27799 return a; 27800 } 27801}; 27802var MouseRotateWrapper = class { 27803 constructor(map, element, pitch = false) { 27804 this.mousedown = (e) => { 27805 this.startMove(e, DOM.mousePos(this.element, e)); 27806 window.addEventListener("mousemove", this.mousemove); 27807 window.addEventListener("mouseup", this.mouseup); 27808 }; 27809 this.mousemove = (e) => { 27810 this.move(e, DOM.mousePos(this.element, e)); 27811 }; 27812 this.mouseup = (e) => { 27813 this._rotatePitchHandler.dragEnd(e); 27814 this.offTemp(); 27815 }; 27816 this.touchstart = (e) => { 27817 if (e.targetTouches.length !== 1) this.reset(); 27818 else { 27819 this._startPos = this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; 27820 this.startMove(e, this._startPos); 27821 window.addEventListener("touchmove", this.touchmove, { passive: false }); 27822 window.addEventListener("touchend", this.touchend); 27823 } 27824 }; 27825 this.touchmove = (e) => { 27826 if (e.targetTouches.length !== 1) this.reset(); 27827 else { 27828 this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; 27829 this.move(e, this._lastPos); 27830 } 27831 }; 27832 this.touchend = (e) => { 27833 if (e.targetTouches.length === 0 && this._startPos && this._lastPos && this._startPos.dist(this._lastPos) < this._clickTolerance) this.element.click(); 27834 delete this._startPos; 27835 delete this._lastPos; 27836 this.offTemp(); 27837 }; 27838 this.reset = () => { 27839 this._rotatePitchHandler.reset(); 27840 delete this._startPos; 27841 delete this._lastPos; 27842 this.offTemp(); 27843 }; 27844 this._clickTolerance = 10; 27845 this.element = element; 27846 const moveStateManager = new MouseOrTouchMoveStateManager(); 27847 this._rotatePitchHandler = new DragHandler({ 27848 clickTolerance: 3, 27849 move: (lastPoint, currentPoint) => { 27850 const rect = element.getBoundingClientRect(); 27851 const center = new Point((rect.bottom - rect.top) / 2, (rect.right - rect.left) / 2); 27852 return { 27853 bearingDelta: getAngleDelta(new Point(lastPoint.x, currentPoint.y), currentPoint, center), 27854 pitchDelta: pitch ? (currentPoint.y - lastPoint.y) * -.5 : void 0 27855 }; 27856 }, 27857 moveStateManager, 27858 enable: true, 27859 assignEvents: () => {} 27860 }); 27861 this.map = map; 27862 element.addEventListener("mousedown", this.mousedown); 27863 element.addEventListener("touchstart", this.touchstart, { passive: false }); 27864 element.addEventListener("touchcancel", this.reset); 27865 } 27866 startMove(e, point) { 27867 this._rotatePitchHandler.dragStart(e, point); 27868 DOM.disableDrag(); 27869 } 27870 move(e, point) { 27871 const map = this.map; 27872 const { bearingDelta, pitchDelta } = this._rotatePitchHandler.dragMove(e, point) || {}; 27873 if (bearingDelta) map.setBearing(map.getBearing() + bearingDelta); 27874 if (pitchDelta) map.setPitch(map.getPitch() + pitchDelta); 27875 } 27876 off() { 27877 const element = this.element; 27878 element.removeEventListener("mousedown", this.mousedown); 27879 element.removeEventListener("touchstart", this.touchstart); 27880 window.removeEventListener("touchmove", this.touchmove); 27881 window.removeEventListener("touchend", this.touchend); 27882 element.removeEventListener("touchcancel", this.reset); 27883 this.offTemp(); 27884 } 27885 offTemp() { 27886 DOM.enableDrag(); 27887 window.removeEventListener("mousemove", this.mousemove); 27888 window.removeEventListener("mouseup", this.mouseup); 27889 window.removeEventListener("touchmove", this.touchmove); 27890 window.removeEventListener("touchend", this.touchend); 27891 } 27892}; 27893//#endregion 27894//#region src/util/geolocation_support.ts 27895let supportsGeolocation; 27896async function checkGeolocationSupport(forceRecalculation = false) { 27897 if (supportsGeolocation !== void 0 && !forceRecalculation) return supportsGeolocation; 27898 if (window.navigator.permissions === void 0) { 27899 supportsGeolocation = !!window.navigator.geolocation; 27900 return supportsGeolocation; 27901 } 27902 try { 27903 supportsGeolocation = (await window.navigator.permissions.query({ name: "geolocation" })).state !== "denied"; 27904 } catch { 27905 supportsGeolocation = !!window.navigator.geolocation; 27906 } 27907 return supportsGeolocation; 27908} 27909//#endregion 27910//#region src/util/smart_wrap.ts 27911/** 27912* Given a LngLat, prior projected position, and a transform, return a new LngLat shifted 27913* n à 360° east or west for some n ⥠0 such that: 27914* 27915* * the projected location of the result is on screen, if possible, and secondarily: 27916* * the difference between the projected location of the result and the prior position 27917* is minimized. 27918* 27919* The object is to preserve perceived object constancy for Popups and Markers as much as 27920* possible; they should avoid shifting large distances across the screen, even when the 27921* map center changes by ±360° due to automatic wrapping, and when about to go off screen, 27922* should wrap just enough to avoid doing so. 27923*/ 27924function smartWrap(lngLat, priorPos, transform, useNormalWrap = false) { 27925 if (useNormalWrap || !transform.getCoveringTilesDetailsProvider().allowWorldCopies()) return lngLat?.wrap(); 27926 const originalLngLat = new LngLat(lngLat.lng, lngLat.lat); 27927 lngLat = new LngLat(lngLat.lng, lngLat.lat); 27928 if (priorPos) { 27929 const left = new LngLat(lngLat.lng - 360, lngLat.lat); 27930 const right = new LngLat(lngLat.lng + 360, lngLat.lat); 27931 const delta = transform.locationToScreenPoint(lngLat).distSqr(priorPos); 27932 if (transform.locationToScreenPoint(left).distSqr(priorPos) < delta) lngLat = left; 27933 else if (transform.locationToScreenPoint(right).distSqr(priorPos) < delta) lngLat = right; 27934 } 27935 while (Math.abs(lngLat.lng - transform.center.lng) > 180) { 27936 const pos = transform.locationToScreenPoint(lngLat); 27937 if (pos.x >= 0 && pos.y >= 0 && pos.x <= transform.width && pos.y <= transform.height) break; 27938 if (lngLat.lng > transform.center.lng) lngLat.lng -= 360; 27939 else lngLat.lng += 360; 27940 } 27941 if (lngLat.lng !== originalLngLat.lng && transform.isPointOnMapSurface(transform.locationToScreenPoint(lngLat))) return lngLat; 27942 return originalLngLat; 27943} 27944//#endregion 27945//#region src/ui/anchor.ts 27946const anchorTranslate = { 27947 "center": "translate(-50%,-50%)", 27948 "top": "translate(-50%,0)", 27949 "top-left": "translate(0,0)", 27950 "top-right": "translate(-100%,0)", 27951 "bottom": "translate(-50%,-100%)", 27952 "bottom-left": "translate(0,-100%)", 27953 "bottom-right": "translate(-100%,-100%)", 27954 "left": "translate(0,-50%)", 27955 "right": "translate(-100%,-50%)" 27956}; 27957function applyAnchorClass(element, anchor, prefix) { 27958 const classList = element.classList; 27959 for (const key in anchorTranslate) classList.remove(`maplibregl-${prefix}-anchor-${key}`); 27960 classList.add(`maplibregl-${prefix}-anchor-${anchor}`); 27961} 27962//#endregion 27963//#region src/ui/marker.ts 27964/** 27965* Screen-pixel deltas applied when a focused draggable marker is moved with the arrow keys. 27966*/ 27967const ARROW_KEY_DELTAS = { 27968 ArrowLeft: [-1, 0], 27969 ArrowRight: [1, 0], 27970 ArrowUp: [0, -1], 27971 ArrowDown: [0, 1] 27972}; 27973const KEYBOARD_DRAG_SMALL_STEP = 1; 27974const KEYBOARD_DRAG_LARGE_STEP = 10; 27975/** Fill color of the default marker when {@link MarkerOptions.color} is not given. */ 27976const DEFAULT_MARKER_COLOR = "#3FB1CE"; 27977/** Height in CSS pixels of the default marker SVG at scale 1. */ 27978const DEFAULT_MARKER_HEIGHT = 41; 27979/** Width in CSS pixels of the default marker SVG at scale 1. */ 27980const DEFAULT_MARKER_WIDTH = 27; 27981const SVG_NS = "http://www.w3.org/2000/svg"; 27982/** Creates an SVG element with the given attributes, set in the order given, and children. */ 27983function svgElement(tag, attributes, children = []) { 27984 const element = DOM.createNS(SVG_NS, tag); 27985 for (const name in attributes) element.setAttributeNS(null, name, attributes[name]); 27986 for (const child of children) element.appendChild(child); 27987 return element; 27988} 27989/** The default marker SVG at scale 1 in the default color, built on first use and cloned per marker. */ 27990let defaultMarkerTemplate; 27991/** 27992* Returns the shared default marker SVG, building it the first time it is asked for. Assembling 27993* the pin takes about fifty DOM calls, so each default {@link Marker} deep-clones this template 27994* and overrides only its size and its fill, see {@link defaultMarkerFillGroup}. 27995*/ 27996function getDefaultMarkerTemplate() { 27997 if (defaultMarkerTemplate) return defaultMarkerTemplate; 27998 const shadow = svgElement("g", { 27999 transform: "translate(3.0, 29.0)", 28000 fill: "#000000" 28001 }, [ 28002 ["10.5", "5.25002273"], 28003 ["10.5", "5.25002273"], 28004 ["9.5", "4.77275007"], 28005 ["8.5", "4.29549936"], 28006 ["7.5", "3.81822308"], 28007 ["6.5", "3.34094679"], 28008 ["5.5", "2.86367051"], 28009 ["4.5", "2.38636864"] 28010 ].map(([rx, ry]) => svgElement("ellipse", { 28011 opacity: "0.04", 28012 cx: "10.5", 28013 cy: "5.80029008", 28014 rx, 28015 ry 28016 }))); 28017 const background = svgElement("g", { fill: DEFAULT_MARKER_COLOR }, [svgElement("path", { d: "M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z" })]); 28018 const border = svgElement("g", { 28019 opacity: "0.25", 28020 fill: "#000000" 28021 }, [svgElement("path", { d: "M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z" })]); 28022 const maki = svgElement("g", { 28023 transform: "translate(6.0, 7.0)", 28024 fill: "#FFFFFF" 28025 }); 28026 const circles = svgElement("g", { transform: "translate(8.0, 8.0)" }, [svgElement("circle", { 28027 fill: "#000000", 28028 opacity: "0.25", 28029 cx: "5.5", 28030 cy: "5.5", 28031 r: "5.4999962" 28032 }), svgElement("circle", { 28033 fill: "#FFFFFF", 28034 cx: "5.5", 28035 cy: "5.5", 28036 r: "5.4999962" 28037 })]); 28038 defaultMarkerTemplate = svgElement("svg", { 28039 display: "block", 28040 height: `${DEFAULT_MARKER_HEIGHT}px`, 28041 width: `${DEFAULT_MARKER_WIDTH}px`, 28042 viewBox: `0 0 ${DEFAULT_MARKER_WIDTH} ${DEFAULT_MARKER_HEIGHT}` 28043 }, [svgElement("g", { "fill-rule": "nonzero" }, [ 28044 shadow, 28045 background, 28046 border, 28047 maki, 28048 circles 28049 ])]); 28050 return defaultMarkerTemplate; 28051} 28052/** The group of a default marker SVG that carries the pin's fill color: the second child of its single top-level group. */ 28053function defaultMarkerFillGroup(svg) { 28054 return svg.firstElementChild.children[1]; 28055} 28056/** 28057* The event class for marker drag events (`dragstart`, `drag` and `dragend`). 28058* 28059* @group Event Related 28060*/ 28061var MarkerDragEvent = class extends Event {}; 28062/** 28063* The event class for the marker `click` event. 28064* 28065* @group Event Related 28066*/ 28067var MarkerClickEvent = class extends Event {}; 28068/** 28069* Creates a marker component 28070* 28071* @group Markers and Controls 28072* 28073* @example 28074* ```ts 28075* let marker = new Marker() 28076* .setLngLat([30.5, 50.5]) 28077* .addTo(map); 28078* ``` 28079* 28080* @example 28081* Set options 28082* ```ts 28083* let marker = new Marker({ 28084* color: "#FFFFFF", 28085* draggable: true 28086* }).setLngLat([30.5, 50.5]) 28087* .addTo(map); 28088* ``` 28089* @see [Add a default marker](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-default-marker/) 28090* @see [Add custom icons with Markers](https://maplibre.org/maplibre-gl-js/docs/examples/add-custom-icons-with-markers/) 28091* @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-marker/) 28092* @see [Animate a marker](https://maplibre.org/maplibre-gl-js/docs/examples/animate-a-marker/) 28093* @see [Attach a popup to a marker instance](https://maplibre.org/maplibre-gl-js/docs/examples/attach-a-popup-to-a-marker-instance/) 28094* 28095* ## Events 28096* 28097* **Event** `dragstart` of type {@link MarkerDragEvent} will be fired when dragging starts. 28098* 28099* **Event** `drag` of type {@link MarkerDragEvent} will be fired while dragging. 28100* 28101* **Event** `dragend` of type {@link MarkerDragEvent} will be fired when the marker is finished being dragged. 28102* 28103* **Event** `click` of type {@link MarkerClickEvent} will be fired when the marker is clicked. 28104* 28105* ## CSS Classes 28106* 28107* **CSS class** `maplibregl-marker-covered` is toggled on the marker element when the marker 28108* is hidden behind 3D terrain or on the back of a globe. 28109* Use this class to apply custom styles to covered markers. 28110* 28111* @example 28112* ```css 28113* .maplibregl-marker-covered { 28114* pointer-events: none; 28115* cursor: default; 28116* } 28117* ``` 28118*/ 28119var Marker = class extends Evented { 28120 /** 28121 * @param options - the options 28122 */ 28123 constructor(options) { 28124 super(); 28125 this._onClick = (e) => { 28126 this.fire(new MarkerClickEvent("click", { originalEvent: e })); 28127 }; 28128 this._onKeyPress = (e) => { 28129 if (e.code === "Space" || e.code === "Enter") this.togglePopup(); 28130 }; 28131 this._onKeyDown = (e) => { 28132 if (!this._defaultMarker || !this._draggable || !this._map || !this._lngLat) return; 28133 if (e.composedPath()[0] !== this._element) return; 28134 if (e.altKey || e.ctrlKey || e.metaKey) return; 28135 const delta = ARROW_KEY_DELTAS[e.key]; 28136 if (!delta) return; 28137 e.preventDefault(); 28138 e.stopPropagation(); 28139 const step = e.shiftKey ? KEYBOARD_DRAG_LARGE_STEP : KEYBOARD_DRAG_SMALL_STEP; 28140 const pos = this._map.project(this._lngLat); 28141 this.setLngLat(this._map.unproject(new Point(pos.x + delta[0] * step, pos.y + delta[1] * step))); 28142 if (!this._keyboardDragActive) { 28143 this._keyboardDragActive = true; 28144 this.fire(new MarkerDragEvent("dragstart")); 28145 } 28146 this.fire(new MarkerDragEvent("drag")); 28147 }; 28148 this._onKeyUp = (e) => { 28149 if (!ARROW_KEY_DELTAS[e.key]) return; 28150 this._endKeyboardDrag(); 28151 }; 28152 this._onBlur = () => { 28153 this._endKeyboardDrag(); 28154 }; 28155 this._onMapClick = (e) => { 28156 const targetElement = e.originalEvent.target; 28157 const element = this._element; 28158 if (this._popup && (targetElement === element || element.contains(targetElement))) this.togglePopup(); 28159 }; 28160 this._updateCoveredUnthrottled = () => { 28161 const terrain = this._map?.terrain; 28162 if (!terrain || !this._isInViewport()) return; 28163 const covered = this._isCovered(terrain); 28164 if (covered && this._popup?.isOpen()) this._popup.remove(); 28165 this._element.style.opacity = covered ? this._opacityWhenCovered : this._opacity; 28166 this._element.classList.toggle("maplibregl-marker-covered", covered); 28167 }; 28168 this._updateCovered = throttle(this._updateCoveredUnthrottled, 100); 28169 this._update = (e) => { 28170 if (!this._map) return; 28171 this._lngLat = smartWrap(this._lngLat, this._flatPos, this._map._camera.transform); 28172 this._flatPos = this._pos = this._map.project(this._lngLat)._add(this._offset); 28173 if (this._map.terrain) this._flatPos = this._map._camera.transform.locationToScreenPoint(this._lngLat)._add(this._offset); 28174 let rotation = ""; 28175 if (this._rotationAlignment === "viewport" || this._rotationAlignment === "auto") rotation = `rotateZ(${this._rotation}deg)`; 28176 else if (this._rotationAlignment === "map") rotation = `rotateZ(${this._rotation - this._map.getBearing()}deg)`; 28177 let pitch = ""; 28178 if (this._pitchAlignment === "viewport" || this._pitchAlignment === "auto") pitch = "rotateX(0deg)"; 28179 else if (this._pitchAlignment === "map") pitch = `rotateX(${this._map.getPitch()}deg)`; 28180 if (!this._subpixelPositioning && e?.type !== "move") this._pos = this._pos.round(); 28181 this._element.style.transform = `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`; 28182 this._updateOpacity(); 28183 }; 28184 this._onMove = (e) => { 28185 if (!this._isDragging) { 28186 const clickTolerance = this._clickTolerance || this._map._clickTolerance; 28187 this._isDragging = e.point.dist(this._pointerdownPos) >= clickTolerance; 28188 } 28189 if (!this._isDragging) return; 28190 this._pos = e.point.sub(this._positionDelta); 28191 this._lngLat = this._map.unproject(this._pos); 28192 this.setLngLat(this._lngLat); 28193 this._element.style.pointerEvents = "none"; 28194 if (this._state === "pending") { 28195 this._state = "active"; 28196 this.fire(new MarkerDragEvent("dragstart")); 28197 } 28198 this.fire(new MarkerDragEvent("drag")); 28199 }; 28200 this._onUp = () => { 28201 this._element.style.pointerEvents = "auto"; 28202 this._positionDelta = null; 28203 this._pointerdownPos = null; 28204 this._isDragging = false; 28205 this._map.off("mousemove", this._onMove); 28206 this._map.off("touchmove", this._onMove); 28207 if (this._state === "active") this.fire(new MarkerDragEvent("dragend")); 28208 this._state = "inactive"; 28209 }; 28210 this._addDragHandler = (e) => { 28211 if (this._element.contains(e.originalEvent.target)) { 28212 e.preventDefault(); 28213 this._positionDelta = e.point.sub(this._pos).add(this._offset); 28214 this._pointerdownPos = e.point; 28215 this._state = "pending"; 28216 this._map.on("mousemove", this._onMove); 28217 this._map.on("touchmove", this._onMove); 28218 this._map.once("mouseup", this._onUp); 28219 this._map.once("touchend", this._onUp); 28220 } 28221 }; 28222 this._anchor = options?.anchor || "center"; 28223 this._color = options?.color || DEFAULT_MARKER_COLOR; 28224 this._scale = options?.scale || 1; 28225 this._draggable = options?.draggable || false; 28226 this._clickTolerance = options?.clickTolerance || 0; 28227 this._subpixelPositioning = options?.subpixelPositioning || false; 28228 this._isDragging = false; 28229 this._roleManaged = false; 28230 this._tabIndexManaged = false; 28231 this._keyboardDragActive = false; 28232 this._state = "inactive"; 28233 this._rotation = options?.rotation || 0; 28234 this._rotationAlignment = options?.rotationAlignment || "auto"; 28235 this._pitchAlignment = options?.pitchAlignment && options.pitchAlignment !== "auto" ? options.pitchAlignment : this._rotationAlignment; 28236 this.setOpacity(options?.opacity, options?.opacityWhenCovered); 28237 if (!options?.element) { 28238 this._defaultMarker = true; 28239 this._element = DOM.create("div"); 28240 const svg = getDefaultMarkerTemplate().cloneNode(true); 28241 svg.setAttributeNS(null, "height", `${DEFAULT_MARKER_HEIGHT * this._scale}px`); 28242 svg.setAttributeNS(null, "width", `${DEFAULT_MARKER_WIDTH * this._scale}px`); 28243 defaultMarkerFillGroup(svg).setAttributeNS(null, "fill", this._color); 28244 this._element.appendChild(svg); 28245 this._offset = Point.convert(options?.offset || [0, -14]); 28246 } else { 28247 this._element = options.element; 28248 this._offset = Point.convert(options?.offset || [0, 0]); 28249 } 28250 this._element.classList.add("maplibregl-marker"); 28251 this._element.addEventListener("dragstart", (e) => { 28252 e.preventDefault(); 28253 }); 28254 this._element.addEventListener("mousedown", (e) => { 28255 e.preventDefault(); 28256 }); 28257 applyAnchorClass(this._element, this._anchor, "marker"); 28258 if (options?.className) for (const name of options.className.split(" ")) this._element.classList.add(name); 28259 this._popup = null; 28260 } 28261 /** 28262 * Attaches the `Marker` to a `Map` object. 28263 * @param map - The MapLibre GL JS map to add the marker to. 28264 * @example 28265 * ```ts 28266 * let marker = new Marker() 28267 * .setLngLat([30.5, 50.5]) 28268 * .addTo(map); // add the marker to the map 28269 * ``` 28270 */ 28271 addTo(map) { 28272 this.remove(); 28273 this._map = map; 28274 if (this._defaultMarker && !this._element.hasAttribute("aria-label")) this._element.setAttribute("aria-label", map._getUIString("Marker.Title")); 28275 this._updateAccessibilityRole(); 28276 map.getCanvasContainer().appendChild(this._element); 28277 map.on("move", this._update); 28278 map.on("moveend", this._update); 28279 map.on("terrain", this._update); 28280 map.on("projectiontransition", this._update); 28281 map.on("idle", this._update); 28282 this._element.addEventListener("click", this._onClick); 28283 this.setDraggable(this._draggable); 28284 this._update(); 28285 this._map.on("click", this._onMapClick); 28286 return this; 28287 } 28288 /** 28289 * Removes the marker from a map 28290 * @example 28291 * ```ts 28292 * let marker = new Marker().addTo(map); 28293 * marker.remove(); 28294 * ``` 28295 */ 28296 remove() { 28297 if (this._map) { 28298 this._map.off("click", this._onMapClick); 28299 this._map.off("move", this._update); 28300 this._map.off("moveend", this._update); 28301 this._map.off("terrain", this._update); 28302 this._map.off("projectiontransition", this._update); 28303 this._map.off("idle", this._update); 28304 this._map.off("mousedown", this._addDragHandler); 28305 this._map.off("touchstart", this._addDragHandler); 28306 this._map.off("mouseup", this._onUp); 28307 this._map.off("touchend", this._onUp); 28308 this._map.off("mousemove", this._onMove); 28309 this._map.off("touchmove", this._onMove); 28310 delete this._map; 28311 } 28312 this._element.removeEventListener("click", this._onClick); 28313 this._element.removeEventListener("keydown", this._onKeyDown); 28314 this._element.removeEventListener("keyup", this._onKeyUp); 28315 this._element.removeEventListener("blur", this._onBlur); 28316 this._element.removeEventListener("keypress", this._onKeyPress); 28317 this._keyboardDragActive = false; 28318 this._element.remove(); 28319 if (this._popup) this._popup.remove(); 28320 return this; 28321 } 28322 /** 28323 * Get the marker's geographical location. 28324 * 28325 * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously 28326 * set by `setLngLat` because `Marker` wraps the anchor longitude across copies of the world to keep 28327 * the marker on screen. 28328 * 28329 * @returns A {@link LngLat} describing the marker's location. 28330 * @example 28331 * ```ts 28332 * // Store the marker's longitude and latitude coordinates in a variable 28333 * let lngLat = marker.getLngLat(); 28334 * // Print the marker's longitude and latitude values in the console 28335 * console.log('Longitude: ' + lngLat.lng + ', Latitude: ' + lngLat.lat ) 28336 * ``` 28337 * @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-marker/) 28338 */ 28339 getLngLat() { 28340 return this._lngLat; 28341 } 28342 /** 28343 * Set the marker's geographical position and move it. 28344 * @param lnglat - A {@link LngLat} describing where the marker should be located. 28345 * @example 28346 * Create a new marker, set the longitude and latitude, and add it to the map 28347 * ```ts 28348 * new Marker() 28349 * .setLngLat([-65.017, -16.457]) 28350 * .addTo(map); 28351 * ``` 28352 * @see [Add custom icons with Markers](https://maplibre.org/maplibre-gl-js/docs/examples/add-custom-icons-with-markers/) 28353 * @see [Create a draggable Marker](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-draggable-marker/) 28354 */ 28355 setLngLat(lnglat) { 28356 this._lngLat = LngLat.convert(lnglat); 28357 this._pos = null; 28358 this._update(); 28359 if (this._popup) this._popup.setLngLat(this._lngLat); 28360 return this; 28361 } 28362 /** 28363 * Returns the `Marker`'s HTML element. 28364 * @returns element 28365 */ 28366 getElement() { 28367 return this._element; 28368 } 28369 /** 28370 * Binds a {@link Popup} to the {@link Marker}. 28371 * @param popup - An instance of the {@link Popup} class. If undefined or null, any popup 28372 * set on this {@link Marker} instance is unset. 28373 * @example 28374 * ```ts 28375 * let marker = new Marker() 28376 * .setLngLat([0, 0]) 28377 * .setPopup(new Popup().setHTML("<h1>Hello World!</h1>")) // add popup 28378 * .addTo(map); 28379 * ``` 28380 * @see [Attach a popup to a marker instance](https://maplibre.org/maplibre-gl-js/docs/examples/attach-a-popup-to-a-marker-instance/) 28381 */ 28382 setPopup(popup) { 28383 if (this._popup) { 28384 this._popup.remove(); 28385 this._popup = null; 28386 this._element.removeEventListener("keypress", this._onKeyPress); 28387 } 28388 if (popup) { 28389 if (!("offset" in popup.options)) { 28390 const markerRadius = 13.5; 28391 const linearOffset = Math.abs(markerRadius) / Math.SQRT2; 28392 popup.options.offset = this._defaultMarker ? { 28393 "top": [0, 0], 28394 "top-left": [0, 0], 28395 "top-right": [0, 0], 28396 "bottom": [0, -38.1], 28397 "bottom-left": [linearOffset, (24.6 + linearOffset) * -1], 28398 "bottom-right": [-linearOffset, (24.6 + linearOffset) * -1], 28399 "left": [markerRadius, -24.6], 28400 "right": [-13.5, -24.6] 28401 } : this._offset; 28402 } 28403 this._popup = popup; 28404 this._element.addEventListener("keypress", this._onKeyPress); 28405 } 28406 this._updateTabIndex(); 28407 this._updateAccessibilityRole(); 28408 return this; 28409 } 28410 /** 28411 * Set the option to allow subpixel positioning of the marker by passing a boolean 28412 * 28413 * @param value - when set to `true`, subpixel positioning is enabled for the marker. 28414 * 28415 * @example 28416 * ```ts 28417 * let marker = new Marker() 28418 * marker.setSubpixelPositioning(true); 28419 * ``` 28420 */ 28421 setSubpixelPositioning(value) { 28422 this._subpixelPositioning = value; 28423 return this; 28424 } 28425 _endKeyboardDrag() { 28426 if (this._keyboardDragActive) { 28427 this._keyboardDragActive = false; 28428 this.fire(new MarkerDragEvent("dragend")); 28429 } 28430 } 28431 /** 28432 * Returns the {@link Popup} instance that is bound to the {@link Marker}. 28433 * @returns popup 28434 * @example 28435 * ```ts 28436 * let marker = new Marker() 28437 * .setLngLat([0, 0]) 28438 * .setPopup(new Popup().setHTML("<h1>Hello World!</h1>")) 28439 * .addTo(map); 28440 * 28441 * console.log(marker.getPopup()); // return the popup instance 28442 * ``` 28443 */ 28444 getPopup() { 28445 return this._popup; 28446 } 28447 /** 28448 * Opens or closes the {@link Popup} instance that is bound to the {@link Marker}, depending on the current state of the {@link Popup}. 28449 * @example 28450 * ```ts 28451 * let marker = new Marker() 28452 * .setLngLat([0, 0]) 28453 * .setPopup(new Popup().setHTML("<h1>Hello World!</h1>")) 28454 * .addTo(map); 28455 * 28456 * marker.togglePopup(); // toggle popup open or closed 28457 * ``` 28458 */ 28459 togglePopup() { 28460 const popup = this._popup; 28461 if (this._element.style.opacity === this._opacityWhenCovered) return this; 28462 if (!popup) return this; 28463 else if (popup.isOpen()) popup.remove(); 28464 else { 28465 popup.setLngLat(this._lngLat); 28466 popup.addTo(this._map); 28467 } 28468 return this; 28469 } 28470 _updateOpacity() { 28471 if (!this._isInViewport()) return; 28472 const terrain = this._map?.terrain; 28473 const occluded = this._map._camera.transform.isLocationOccluded(this._lngLat); 28474 if (!terrain || occluded) { 28475 const targetOpacity = occluded ? this._opacityWhenCovered : this._opacity; 28476 if (this._element.style.opacity !== targetOpacity) { 28477 this._element.style.opacity = targetOpacity; 28478 this._element.classList.toggle("maplibregl-marker-covered", occluded); 28479 } 28480 return; 28481 } 28482 this._updateCovered(); 28483 } 28484 /** 28485 * @internal 28486 * Whether the viewport shows the marker's position. There is nothing to compute for one it does not. 28487 */ 28488 _isInViewport() { 28489 const { width, height } = this._map._camera.transform; 28490 return !!this._pos && this._pos.x >= 0 && this._pos.y >= 0 && this._pos.x <= width && this._pos.y <= height; 28491 } 28492 /** 28493 * @internal 28494 * Whether the terrain covers the marker's center: the location raised by the height `offset` lifts the element 28495 * above it on screen, or the location itself when the offset lowers the element. 28496 */ 28497 _isCovered(terrain) { 28498 const transform = this._map._camera.transform; 28499 const elevation = terrain.getElevationForLngLat(this._lngLat, transform); 28500 const metersToCenter = Math.max(0, -this._offset.y) / transform.pixelsPerMeter; 28501 const elevationToCenter = Math.sin(this._map.getPitch() * Math.PI / 180) * metersToCenter; 28502 return transform.isLocationOccluded(this._lngLat, terrain, elevation + elevationToCenter); 28503 } 28504 /** 28505 * Get the marker's offset. 28506 * @returns The marker's screen coordinates in pixels. 28507 */ 28508 getOffset() { 28509 return this._offset; 28510 } 28511 /** 28512 * Sets the offset of the marker 28513 * @param offset - The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up. 28514 */ 28515 setOffset(offset) { 28516 this._offset = Point.convert(offset); 28517 this._update(); 28518 return this; 28519 } 28520 /** 28521 * Adds a CSS class to the marker element. 28522 * 28523 * @param className - on-empty string with CSS class name to add to marker element 28524 * 28525 * @example 28526 * ``` 28527 * let marker = new Marker() 28528 * marker.addClassName('some-class') 28529 * ``` 28530 */ 28531 addClassName(className) { 28532 this._element.classList.add(className); 28533 } 28534 /** 28535 * Removes a CSS class from the marker element. 28536 * 28537 * @param className - Non-empty string with CSS class name to remove from marker element 28538 * 28539 * @example 28540 * ```ts 28541 * let marker = new Marker() 28542 * marker.removeClassName('some-class') 28543 * ``` 28544 */ 28545 removeClassName(className) { 28546 this._element.classList.remove(className); 28547 } 28548 /** 28549 * Add or remove the given CSS class on the marker element, depending on whether the element currently has that class. 28550 * 28551 * @param className - Non-empty string with CSS class name to add/remove 28552 * 28553 * @returns if the class was removed return false, if class was added, then return true 28554 * 28555 * @example 28556 * ```ts 28557 * let marker = new Marker() 28558 * marker.toggleClassName('toggleClass') 28559 * ``` 28560 */ 28561 toggleClassName(className) { 28562 return this._element.classList.toggle(className); 28563 } 28564 /** 28565 * Sets the `draggable` property and functionality of the marker. 28566 * A draggable default marker is also keyboard focusable and movable with the 28567 * arrow keys (see {@link MarkerOptions.draggable}); custom marker elements keep 28568 * their focusability and keyboard behavior application-owned. 28569 * @param shouldBeDraggable - Turns drag functionality on/off 28570 */ 28571 setDraggable(shouldBeDraggable) { 28572 this._draggable = !!shouldBeDraggable; 28573 this._element.classList.toggle("maplibregl-marker-draggable", this._draggable); 28574 if (this._map) { 28575 if (shouldBeDraggable) { 28576 this._map.on("mousedown", this._addDragHandler); 28577 this._map.on("touchstart", this._addDragHandler); 28578 } else { 28579 this._map.off("mousedown", this._addDragHandler); 28580 this._map.off("touchstart", this._addDragHandler); 28581 } 28582 } 28583 if (this._defaultMarker) { 28584 if (this._draggable) { 28585 this._element.addEventListener("keydown", this._onKeyDown); 28586 this._element.addEventListener("keyup", this._onKeyUp); 28587 this._element.addEventListener("blur", this._onBlur); 28588 } else { 28589 this._element.removeEventListener("keydown", this._onKeyDown); 28590 this._element.removeEventListener("keyup", this._onKeyUp); 28591 this._element.removeEventListener("blur", this._onBlur); 28592 this._endKeyboardDrag(); 28593 } 28594 } 28595 this._updateTabIndex(); 28596 this._updateAccessibilityRole(); 28597 return this; 28598 } 28599 /** 28600 * Returns true if the marker can be dragged 28601 * @returns True if the marker is draggable. 28602 */ 28603 isDraggable() { 28604 return this._draggable; 28605 } 28606 /** 28607 * Keep the marker element focusable while it has built-in keyboard behavior. 28608 * A popup makes any marker element interactive; dragging only manages 28609 * focusability for the default marker so custom elements stay 28610 * application-owned (#7790). A tabindex supplied by the application is 28611 * never added, changed, or removed here. 28612 */ 28613 _updateTabIndex() { 28614 if (!!this._popup || this._defaultMarker && this._draggable) { 28615 if (!this._element.hasAttribute("tabindex")) { 28616 this._element.setAttribute("tabindex", "0"); 28617 this._tabIndexManaged = true; 28618 } 28619 } else if (this._tabIndexManaged) { 28620 if (this._element.getAttribute("tabindex") === "0") this._element.removeAttribute("tabindex"); 28621 this._tabIndexManaged = false; 28622 } 28623 } 28624 /** 28625 * Keep the default marker role aligned with interactivity. 28626 * Default markers need a role because `aria-label` is set in {@link Marker.addTo}. 28627 * Non-interactive markers use `role=img`; interactive ones (draggable or with a popup) use `role=button`. 28628 * Click listeners are application-owned and do not automatically change the role. 28629 * Custom marker elements are left alone so applications own their a11y tree. 28630 * Explicit roles set by the application are preserved. 28631 */ 28632 _updateAccessibilityRole() { 28633 if (!this._defaultMarker) return; 28634 if (this._element.hasAttribute("role") && !this._roleManaged) return; 28635 const role = this._draggable || !!this._popup ? "button" : "img"; 28636 this._element.setAttribute("role", role); 28637 this._roleManaged = true; 28638 } 28639 /** 28640 * Sets the `rotation` property of the marker. 28641 * @param rotation - The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker.setRotationAlignment} setting. 28642 */ 28643 setRotation(rotation) { 28644 this._rotation = rotation || 0; 28645 this._update(); 28646 return this; 28647 } 28648 /** 28649 * Returns the current rotation angle of the marker (in degrees). 28650 * @returns The current rotation angle of the marker. 28651 */ 28652 getRotation() { 28653 return this._rotation; 28654 } 28655 /** 28656 * Sets the `rotationAlignment` property of the marker. 28657 * @param alignment - Sets the `rotationAlignment` property of the marker. defaults to 'auto' 28658 */ 28659 setRotationAlignment(alignment) { 28660 this._rotationAlignment = alignment || "auto"; 28661 this._update(); 28662 return this; 28663 } 28664 /** 28665 * Returns the current `rotationAlignment` property of the marker. 28666 * @returns The current rotational alignment of the marker. 28667 */ 28668 getRotationAlignment() { 28669 return this._rotationAlignment; 28670 } 28671 /** 28672 * Sets the `pitchAlignment` property of the marker. 28673 * @param alignment - Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`. 28674 */ 28675 setPitchAlignment(alignment) { 28676 this._pitchAlignment = alignment && alignment !== "auto" ? alignment : this._rotationAlignment; 28677 this._update(); 28678 return this; 28679 } 28680 /** 28681 * Returns the current `pitchAlignment` property of the marker. 28682 * @returns The current pitch alignment of the marker in degrees. 28683 */ 28684 getPitchAlignment() { 28685 return this._pitchAlignment; 28686 } 28687 /** 28688 * Sets the `opacity` and `opacityWhenCovered` properties of the marker. 28689 * When called without arguments, resets opacity and opacityWhenCovered to defaults 28690 * @param opacity - Sets the `opacity` property of the marker. 28691 * @param opacityWhenCovered - Sets the `opacityWhenCovered` property of the marker. 28692 */ 28693 setOpacity(opacity, opacityWhenCovered) { 28694 if (this._opacity === void 0 || opacity === void 0 && opacityWhenCovered === void 0) { 28695 this._opacity = "1"; 28696 this._opacityWhenCovered = "0.2"; 28697 } 28698 if (opacity !== void 0) this._opacity = String(opacity); 28699 if (opacityWhenCovered !== void 0) this._opacityWhenCovered = String(opacityWhenCovered); 28700 if (this._map) this._updateOpacity(); 28701 return this; 28702 } 28703}; 28704//#endregion 28705//#region src/ui/control/geolocate_control.ts 28706const defaultOptions$2 = { 28707 positionOptions: { 28708 enableHighAccuracy: false, 28709 maximumAge: 0, 28710 timeout: 6e3 28711 }, 28712 fitBoundsOptions: { maxZoom: 15 }, 28713 trackUserLocation: false, 28714 showAccuracyCircle: true, 28715 showUserLocation: true 28716}; 28717let numberOfWatches = 0; 28718let noTimeout = false; 28719/** 28720* The event class for geolocate control state events 28721* (`trackuserlocationstart`, `trackuserlocationend`, `userlocationfocus` and `userlocationlostfocus`). 28722* 28723* @group Event Related 28724*/ 28725var GeolocateEvent = class extends Event {}; 28726/** 28727* The event class for the geolocate control `geolocate` and `outofmaxbounds` events. 28728* Carries the [Position](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition) returned by the Geolocation API. 28729* 28730* @group Event Related 28731*/ 28732var GeolocatePositionEvent = class extends Event {}; 28733/** 28734* The event class for the geolocate control `error` event. 28735* Carries the [PositionError](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError) returned by the Geolocation API. 28736* 28737* @group Event Related 28738*/ 28739var GeolocateErrorEvent = class extends Event {}; 28740/** 28741* A `GeolocateControl` control provides a button that uses the browser's geolocation 28742* API to locate the user on the map. 28743* 28744* Not all browsers support geolocation, 28745* and some users may disable the feature. Geolocation support for modern 28746* browsers including Chrome requires sites to be served over HTTPS. If 28747* geolocation support is not available, the `GeolocateControl` will show 28748* as disabled. 28749* 28750* The zoom level applied will depend on the accuracy of the geolocation provided by the device. 28751* 28752* The `GeolocateControl` has two modes. If `trackUserLocation` is `false` (default) the control acts as a button, which when pressed will set the map's camera to target the user location. If the user moves, the map won't update. This is most suited for the desktop. If `trackUserLocation` is `true` the control acts as a toggle button that when active the user's location is actively monitored for changes. In this mode the `GeolocateControl` has three interaction states: 28753* * active - the map's camera automatically updates as the user's location changes, keeping the location dot in the center. Initial state and upon clicking the `GeolocateControl` button. 28754* * passive - the user's location dot automatically updates, but the map's camera does not. Occurs upon the user initiating a map movement. 28755* * disabled - occurs if Geolocation is not available, disabled or denied. 28756* 28757* These interaction states can't be controlled programmatically, rather they are set based on user interactions. 28758* 28759* ## State Diagram 28760*  28761* 28762* @group Markers and Controls 28763* 28764* @example 28765* ```ts 28766* map.addControl(new GeolocateControl({ 28767* positionOptions: { 28768* enableHighAccuracy: true 28769* }, 28770* trackUserLocation: true 28771* })); 28772* ``` 28773* @see [Locate the user](https://maplibre.org/maplibre-gl-js/docs/examples/locate-the-user/) 28774* 28775* ## Events 28776* 28777* **Event** `trackuserlocationend` of type {@link Event} will be fired when the `GeolocateControl` changes to the background state, which happens when a user changes the camera during an active position lock. This only applies when `trackUserLocation` is `true`. In the background state, the dot on the map will update with location updates but the camera will not. 28778* 28779* **Event** `trackuserlocationstart` of type {@link Event} will be fired when the `GeolocateControl` changes to the active lock state, which happens either upon first obtaining a successful Geolocation API position for the user (a `geolocate` event will follow), or the user clicks the geolocate button when in the background state which uses the last known position to recenter the map and enter active lock state (no `geolocate` event will follow unless the users's location changes). 28780* 28781* **Event** `userlocationlostfocus` of type {@link Event} will be fired when the `GeolocateControl` changes to the background state, which happens when a user changes the camera during an active position lock. This only applies when `trackUserLocation` is `true`. In the background state, the dot on the map will update with location updates but the camera will not. 28782* 28783* **Event** `userlocationfocus` of type {@link Event} will be fired when the `GeolocateControl` changes to the active lock state, which happens upon the user clicks the geolocate button when in the background state which uses the last known position to recenter the map and enter active lock state. 28784* 28785* **Event** `geolocate` of type {@link Event} will be fired on each Geolocation API position update which returned as success. 28786* `data` - The returned [Position](https://developer.mozilla.org/en-US/docs/Web/API/Position) object from the callback in [Geolocation.getCurrentPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) or [Geolocation.watchPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition). 28787* 28788* **Event** `error` of type {@link Event} will be fired on each Geolocation API position update which returned as an error. 28789* `data` - The returned [PositionError](https://developer.mozilla.org/en-US/docs/Web/API/PositionError) object from the callback in [Geolocation.getCurrentPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) or [Geolocation.watchPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition). 28790* 28791* **Event** `outofmaxbounds` of type {@link Event} will be fired on each Geolocation API position update which returned as success but user position is out of map `maxBounds`. 28792* `data` - The returned [Position](https://developer.mozilla.org/en-US/docs/Web/API/Position) object from the callback in [Geolocation.getCurrentPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) or [Geolocation.watchPosition()](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition). 28793* 28794* @example 28795* ```ts 28796* // Initialize the geolocate control. 28797* let geolocate = new GeolocateControl({ 28798* positionOptions: { 28799* enableHighAccuracy: true 28800* }, 28801* trackUserLocation: true 28802* }); 28803* // Add the control to the map. 28804* map.addControl(geolocate); 28805* // Set an event listener that fires 28806* // when a trackuserlocationend event occurs. 28807* geolocate.on('trackuserlocationend', () => { 28808* console.log('A trackuserlocationend event has occurred.') 28809* }); 28810* ``` 28811* 28812* @example 28813* ```ts 28814* // Initialize the geolocate control. 28815* let geolocate = new GeolocateControl({ 28816* positionOptions: { 28817* enableHighAccuracy: true 28818* }, 28819* trackUserLocation: true 28820* }); 28821* // Add the control to the map. 28822* map.addControl(geolocate); 28823* // Set an event listener that fires 28824* // when a trackuserlocationstart event occurs. 28825* geolocate.on('trackuserlocationstart', () => { 28826* console.log('A trackuserlocationstart event has occurred.') 28827* }); 28828* ``` 28829* 28830* @example 28831* ```ts 28832* // Initialize the geolocate control. 28833* let geolocate = new GeolocateControl({ 28834* positionOptions: { 28835* enableHighAccuracy: true 28836* }, 28837* trackUserLocation: true 28838* }); 28839* // Add the control to the map. 28840* map.addControl(geolocate); 28841* // Set an event listener that fires 28842* // when an userlocationlostfocus event occurs. 28843* geolocate.on('userlocationlostfocus', function() { 28844* console.log('An userlocationlostfocus event has occurred.') 28845* }); 28846* ``` 28847* 28848* @example 28849* ```ts 28850* // Initialize the geolocate control. 28851* let geolocate = new GeolocateControl({ 28852* positionOptions: { 28853* enableHighAccuracy: true 28854* }, 28855* trackUserLocation: true 28856* }); 28857* // Add the control to the map. 28858* map.addControl(geolocate); 28859* // Set an event listener that fires 28860* // when an userlocationfocus event occurs. 28861* geolocate.on('userlocationfocus', function() { 28862* console.log('An userlocationfocus event has occurred.') 28863* }); 28864* ``` 28865* 28866* @example 28867* ```ts 28868* // Initialize the geolocate control. 28869* let geolocate = new GeolocateControl({ 28870* positionOptions: { 28871* enableHighAccuracy: true 28872* }, 28873* trackUserLocation: true 28874* }); 28875* // Add the control to the map. 28876* map.addControl(geolocate); 28877* // Set an event listener that fires 28878* // when a geolocate event occurs. 28879* geolocate.on('geolocate', () => { 28880* console.log('A geolocate event has occurred.') 28881* }); 28882* ``` 28883* 28884* @example 28885* ```ts 28886* // Initialize the geolocate control. 28887* let geolocate = new GeolocateControl({ 28888* positionOptions: { 28889* enableHighAccuracy: true 28890* }, 28891* trackUserLocation: true 28892* }); 28893* // Add the control to the map. 28894* map.addControl(geolocate); 28895* // Set an event listener that fires 28896* // when an error event occurs. 28897* geolocate.on('error', () => { 28898* console.log('An error event has occurred.') 28899* }); 28900* ``` 28901* 28902* @example 28903* ```ts 28904* // Initialize the geolocate control. 28905* let geolocate = new GeolocateControl({ 28906* positionOptions: { 28907* enableHighAccuracy: true 28908* }, 28909* trackUserLocation: true 28910* }); 28911* // Add the control to the map. 28912* map.addControl(geolocate); 28913* // Set an event listener that fires 28914* // when an outofmaxbounds event occurs. 28915* geolocate.on('outofmaxbounds', () => { 28916* console.log('An outofmaxbounds event has occurred.') 28917* }); 28918* ``` 28919*/ 28920var GeolocateControl = class extends Evented { 28921 /** 28922 * @param options - the control's options 28923 */ 28924 constructor(options) { 28925 super(); 28926 this._onSuccess = (position) => { 28927 if (!this._map) return; 28928 if (this._isOutOfMapMaxBounds(position)) { 28929 this._setErrorState(); 28930 this.fire(new GeolocatePositionEvent("outofmaxbounds", position)); 28931 this._updateMarker(); 28932 this._finish(); 28933 return; 28934 } 28935 if (this.options.trackUserLocation) { 28936 this._lastKnownPosition = position; 28937 switch (this._watchState) { 28938 case "WAITING_ACTIVE": 28939 case "ACTIVE_LOCK": 28940 case "ACTIVE_ERROR": 28941 this._watchState = "ACTIVE_LOCK"; 28942 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"); 28943 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"); 28944 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active"); 28945 break; 28946 case "BACKGROUND": 28947 case "BACKGROUND_ERROR": 28948 this._watchState = "BACKGROUND"; 28949 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"); 28950 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"); 28951 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background"); 28952 break; 28953 default: throw new Error(`Unexpected watchState ${this._watchState}`); 28954 } 28955 } 28956 if (this.options.showUserLocation && this._watchState !== "OFF") this._updateMarker(position); 28957 if (!this.options.trackUserLocation || this._watchState === "ACTIVE_LOCK") this._updateCamera(position); 28958 if (this.options.showUserLocation) this._dotElement.classList.remove("maplibregl-user-location-dot-stale"); 28959 this.fire(new GeolocatePositionEvent("geolocate", position)); 28960 this._finish(); 28961 }; 28962 this._updateCamera = (position) => { 28963 const center = new LngLat(position.coords.longitude, position.coords.latitude); 28964 const radius = position.coords.accuracy; 28965 const bearing = this._map.getBearing(); 28966 const options = extend({ bearing }, this.options.fitBoundsOptions); 28967 const newBounds = LngLatBounds.fromLngLat(center, radius); 28968 this._map.fitBounds(newBounds, options, { geolocateSource: true }); 28969 }; 28970 this._updateMarker = (position) => { 28971 if (position) { 28972 const center = new LngLat(position.coords.longitude, position.coords.latitude); 28973 this._accuracyCircleMarker.setLngLat(center).addTo(this._map); 28974 this._userLocationDotMarker.setLngLat(center).addTo(this._map); 28975 this._accuracy = position.coords.accuracy; 28976 this._updateCircleRadiusIfNeeded(); 28977 } else { 28978 this._userLocationDotMarker.remove(); 28979 this._accuracyCircleMarker.remove(); 28980 } 28981 }; 28982 this._onUpdate = () => { 28983 this._updateCircleRadiusIfNeeded(); 28984 }; 28985 this._onError = (error) => { 28986 if (!this._map) return; 28987 if (error.code === 1) { 28988 this._watchState = "OFF"; 28989 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"); 28990 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"); 28991 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"); 28992 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"); 28993 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"); 28994 this._geolocateButton.disabled = true; 28995 const title = this._map._getUIString("GeolocateControl.LocationNotAvailable"); 28996 this._geolocateButton.title = title; 28997 this._geolocateButton.setAttribute("aria-label", title); 28998 if (this._geolocationWatchID !== void 0) this._clearWatch(); 28999 } else if (error.code === 3 && noTimeout) return; 29000 else this._setErrorState(); 29001 if (this._watchState !== "OFF" && this.options.showUserLocation) this._dotElement.classList.add("maplibregl-user-location-dot-stale"); 29002 this.fire(new GeolocateErrorEvent("error", error)); 29003 this._finish(); 29004 }; 29005 this._finish = () => { 29006 if (this._timeoutId) clearTimeout(this._timeoutId); 29007 this._timeoutId = void 0; 29008 }; 29009 this._onMoveStart = (event) => { 29010 if (!this._map) return; 29011 const fromResize = event?.[0] instanceof ResizeObserverEntry; 29012 if (!event.geolocateSource && this._watchState === "ACTIVE_LOCK" && !fromResize && !this._map.isZooming()) { 29013 this._watchState = "BACKGROUND"; 29014 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background"); 29015 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"); 29016 this.fire(new GeolocateEvent("trackuserlocationend")); 29017 this.fire(new GeolocateEvent("userlocationlostfocus")); 29018 } 29019 }; 29020 this._setupUI = () => { 29021 if (!this._map) return; 29022 this._container.addEventListener("contextmenu", (e) => { 29023 e.preventDefault(); 29024 }); 29025 this._geolocateButton = DOM.create("button", "maplibregl-ctrl-geolocate", this._container); 29026 DOM.create("span", "maplibregl-ctrl-icon", this._geolocateButton).setAttribute("aria-hidden", "true"); 29027 this._geolocateButton.type = "button"; 29028 this._geolocateButton.disabled = true; 29029 }; 29030 this._finishSetupUI = (supported) => { 29031 if (!this._map) return; 29032 if (supported === false) { 29033 warnOnce("Geolocation support is not available so the GeolocateControl will be disabled."); 29034 const title = this._map._getUIString("GeolocateControl.LocationNotAvailable"); 29035 this._geolocateButton.disabled = true; 29036 this._geolocateButton.title = title; 29037 this._geolocateButton.setAttribute("aria-label", title); 29038 } else { 29039 const title = this._map._getUIString("GeolocateControl.FindMyLocation"); 29040 this._geolocateButton.disabled = false; 29041 this._geolocateButton.title = title; 29042 this._geolocateButton.setAttribute("aria-label", title); 29043 } 29044 if (this.options.trackUserLocation) { 29045 this._geolocateButton.setAttribute("aria-pressed", "false"); 29046 this._watchState = "OFF"; 29047 } 29048 if (this.options.showUserLocation) { 29049 this._dotElement = DOM.create("div", "maplibregl-user-location-dot"); 29050 this._userLocationDotMarker = new Marker({ element: this._dotElement }); 29051 this._circleElement = DOM.create("div", "maplibregl-user-location-accuracy-circle"); 29052 this._accuracyCircleMarker = new Marker({ 29053 element: this._circleElement, 29054 pitchAlignment: "map" 29055 }); 29056 if (this.options.trackUserLocation) this._watchState = "OFF"; 29057 this._map.on("zoom", this._onUpdate); 29058 this._map.on("move", this._onUpdate); 29059 this._map.on("rotate", this._onUpdate); 29060 this._map.on("pitch", this._onUpdate); 29061 } 29062 this._geolocateButton.addEventListener("click", () => this.trigger()); 29063 this._setup = true; 29064 if (this.options.trackUserLocation) this._map.on("movestart", this._onMoveStart); 29065 }; 29066 this.options = extend({}, defaultOptions$2, options); 29067 } 29068 /** {@inheritDoc IControl.onAdd} */ 29069 onAdd(map) { 29070 this._map = map; 29071 this._container = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-group"); 29072 this._setupUI(); 29073 checkGeolocationSupport().then((supported) => this._finishSetupUI(supported)); 29074 return this._container; 29075 } 29076 /** {@inheritDoc IControl.onRemove} */ 29077 onRemove() { 29078 if (this._geolocationWatchID !== void 0) { 29079 window.navigator.geolocation.clearWatch(this._geolocationWatchID); 29080 this._geolocationWatchID = void 0; 29081 } 29082 if (this.options.showUserLocation && this._userLocationDotMarker) this._userLocationDotMarker.remove(); 29083 if (this.options.showAccuracyCircle && this._accuracyCircleMarker) this._accuracyCircleMarker.remove(); 29084 this._container.remove(); 29085 this._map.off("movestart", this._onMoveStart); 29086 this._map.off("zoom", this._onUpdate); 29087 this._map.off("move", this._onUpdate); 29088 this._map.off("rotate", this._onUpdate); 29089 this._map.off("pitch", this._onUpdate); 29090 this._map = void 0; 29091 numberOfWatches = 0; 29092 noTimeout = false; 29093 } 29094 /** 29095 * Check if the Geolocation API Position is outside the map's `maxBounds`. 29096 * 29097 * @param position - the Geolocation API Position 29098 * @returns `true` if position is outside the map's `maxBounds`, otherwise returns `false`. 29099 */ 29100 _isOutOfMapMaxBounds(position) { 29101 const bounds = this._map.getMaxBounds(); 29102 const coordinates = position.coords; 29103 return bounds && (coordinates.longitude < bounds.getWest() || coordinates.longitude > bounds.getEast() || coordinates.latitude < bounds.getSouth() || coordinates.latitude > bounds.getNorth()); 29104 } 29105 _setErrorState() { 29106 switch (this._watchState) { 29107 case "WAITING_ACTIVE": 29108 this._watchState = "ACTIVE_ERROR"; 29109 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"); 29110 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error"); 29111 break; 29112 case "ACTIVE_LOCK": 29113 this._watchState = "ACTIVE_ERROR"; 29114 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"); 29115 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error"); 29116 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"); 29117 break; 29118 case "BACKGROUND": 29119 this._watchState = "BACKGROUND_ERROR"; 29120 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"); 29121 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background-error"); 29122 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"); 29123 break; 29124 case "ACTIVE_ERROR": 29125 case "BACKGROUND_ERROR": break; 29126 case "OFF": 29127 case void 0: break; 29128 default: throw new Error(`Unexpected watchState ${this._watchState}`); 29129 } 29130 } 29131 _updateCircleRadiusIfNeeded() { 29132 const userLocation = this._userLocationDotMarker.getLngLat(); 29133 if (!this.options.showUserLocation || !this.options.showAccuracyCircle || !this._accuracy || !userLocation) return; 29134 const screenPosition = this._map.project(userLocation); 29135 const userLocationWith100Px = this._map.unproject([screenPosition.x + 100, screenPosition.y]); 29136 const pixelsToMeters = userLocation.distanceTo(userLocationWith100Px) / 100; 29137 const circleDiameter = 2 * this._accuracy / pixelsToMeters; 29138 this._circleElement.style.width = `${circleDiameter.toFixed(2)}px`; 29139 this._circleElement.style.height = `${circleDiameter.toFixed(2)}px`; 29140 } 29141 /** 29142 * Programmatically request and move the map to the user's location. 29143 * 29144 * @returns `false` if called before control was added to a map, otherwise returns `true`. 29145 * @example 29146 * ```ts 29147 * // Initialize the geolocate control. 29148 * let geolocate = new GeolocateControl({ 29149 * positionOptions: { 29150 * enableHighAccuracy: true 29151 * }, 29152 * trackUserLocation: true 29153 * }); 29154 * // Add the control to the map. 29155 * map.addControl(geolocate); 29156 * map.on('load', () => { 29157 * geolocate.trigger(); 29158 * }); 29159 * ``` 29160 */ 29161 trigger() { 29162 if (!this._setup) { 29163 warnOnce("Geolocate control triggered before added to a map"); 29164 return false; 29165 } 29166 if (this.options.trackUserLocation) { 29167 switch (this._watchState) { 29168 case "OFF": 29169 this._watchState = "WAITING_ACTIVE"; 29170 this.fire(new GeolocateEvent("trackuserlocationstart")); 29171 break; 29172 case "WAITING_ACTIVE": 29173 case "ACTIVE_LOCK": 29174 case "ACTIVE_ERROR": 29175 case "BACKGROUND_ERROR": 29176 numberOfWatches--; 29177 noTimeout = false; 29178 this._watchState = "OFF"; 29179 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"); 29180 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"); 29181 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"); 29182 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"); 29183 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"); 29184 this.fire(new GeolocateEvent("trackuserlocationend")); 29185 break; 29186 case "BACKGROUND": 29187 this._watchState = "ACTIVE_LOCK"; 29188 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"); 29189 if (this._lastKnownPosition) this._updateCamera(this._lastKnownPosition); 29190 this.fire(new GeolocateEvent("trackuserlocationstart")); 29191 this.fire(new GeolocateEvent("userlocationfocus")); 29192 break; 29193 default: throw new Error(`Unexpected watchState ${this._watchState}`); 29194 } 29195 switch (this._watchState) { 29196 case "WAITING_ACTIVE": 29197 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"); 29198 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active"); 29199 break; 29200 case "ACTIVE_LOCK": 29201 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active"); 29202 break; 29203 case "OFF": break; 29204 default: throw new Error(`Unexpected watchState ${this._watchState}`); 29205 } 29206 if (this._watchState === "OFF" && this._geolocationWatchID !== void 0) this._clearWatch(); 29207 else if (this._geolocationWatchID === void 0) { 29208 this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"); 29209 this._geolocateButton.setAttribute("aria-pressed", "true"); 29210 numberOfWatches++; 29211 let positionOptions; 29212 if (numberOfWatches > 1) { 29213 positionOptions = { 29214 maximumAge: 6e5, 29215 timeout: 0 29216 }; 29217 noTimeout = true; 29218 } else { 29219 positionOptions = this.options.positionOptions; 29220 noTimeout = false; 29221 } 29222 this._geolocationWatchID = window.navigator.geolocation.watchPosition(this._onSuccess, this._onError, positionOptions); 29223 } 29224 } else { 29225 window.navigator.geolocation.getCurrentPosition(this._onSuccess, this._onError, this.options.positionOptions); 29226 this._timeoutId = setTimeout(this._finish, 1e4); 29227 } 29228 return true; 29229 } 29230 _clearWatch() { 29231 window.navigator.geolocation.clearWatch(this._geolocationWatchID); 29232 this._geolocationWatchID = void 0; 29233 this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"); 29234 this._geolocateButton.setAttribute("aria-pressed", "false"); 29235 if (this.options.showUserLocation) this._updateMarker(null); 29236 } 29237}; 29238//#endregion 29239//#region src/ui/control/scale_control.ts 29240const defaultOptions$1 = { 29241 maxWidth: 100, 29242 unit: "metric" 29243}; 29244/** 29245* A `ScaleControl` control displays the ratio of a distance on the map to the corresponding distance on the ground. 29246* 29247* @group Markers and Controls 29248* 29249* @example 29250* ```ts 29251* let scale = new ScaleControl({ 29252* maxWidth: 80, 29253* unit: 'imperial' 29254* }); 29255* map.addControl(scale); 29256* 29257* scale.setUnit('metric'); 29258* ``` 29259*/ 29260var ScaleControl = class { 29261 /** 29262 * @param options - the control's options 29263 */ 29264 constructor(options) { 29265 this._onMove = () => { 29266 updateScale(this._map, this._container, this.options); 29267 }; 29268 this.setUnit = (unit) => { 29269 this.options.unit = unit; 29270 updateScale(this._map, this._container, this.options); 29271 }; 29272 this.options = { 29273 ...defaultOptions$1, 29274 ...options 29275 }; 29276 } 29277 getDefaultPosition() { 29278 return "bottom-left"; 29279 } 29280 /** {@inheritDoc IControl.onAdd} */ 29281 onAdd(map) { 29282 this._map = map; 29283 this._container = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-scale", map.getContainer()); 29284 this._map.on("move", this._onMove); 29285 this._onMove(); 29286 return this._container; 29287 } 29288 /** {@inheritDoc IControl.onRemove} */ 29289 onRemove() { 29290 this._container.remove(); 29291 this._map.off("move", this._onMove); 29292 this._map = void 0; 29293 } 29294}; 29295function updateScale(map, container, options) { 29296 const optWidth = options?.maxWidth || 100; 29297 const { width: containerWidth, height: containerHeight } = map._camera.transform; 29298 const y = containerHeight / 2; 29299 const x = containerWidth / 2; 29300 const left = map.unproject([x - optWidth / 2, y]); 29301 const right = map.unproject([x + optWidth / 2, y]); 29302 const globeWidth = Math.round(map.project(right).x - map.project(left).x); 29303 const maxWidth = Math.min(optWidth, globeWidth, containerWidth); 29304 const maxMeters = left.distanceTo(right); 29305 if (options?.unit === "imperial") { 29306 const maxFeet = 3.2808 * maxMeters; 29307 if (maxFeet > 5280) setScale(container, maxWidth, maxFeet / 5280, map._getUIString("ScaleControl.Miles")); 29308 else setScale(container, maxWidth, maxFeet, map._getUIString("ScaleControl.Feet")); 29309 } else if (options?.unit === "nautical") setScale(container, maxWidth, maxMeters / 1852, map._getUIString("ScaleControl.NauticalMiles")); 29310 else if (maxMeters >= 1e3) setScale(container, maxWidth, maxMeters / 1e3, map._getUIString("ScaleControl.Kilometers")); 29311 else setScale(container, maxWidth, maxMeters, map._getUIString("ScaleControl.Meters")); 29312} 29313function setScale(container, maxWidth, maxDistance, unit) { 29314 const distance = getRoundNum(maxDistance); 29315 const ratio = distance / maxDistance; 29316 container.style.width = `${maxWidth * ratio}px`; 29317 const label = `${distance}\u00a0${unit}`; 29318 if (container.textContent !== label) container.textContent = label; 29319} 29320function getDecimalRoundNum(d) { 29321 const multiplier = Math.pow(10, Math.ceil(-Math.log(d) / Math.LN10)); 29322 return Math.round(d * multiplier) / multiplier; 29323} 29324function getRoundNum(num) { 29325 const pow10 = Math.pow(10, `${Math.floor(num)}`.length - 1); 29326 let d = num / pow10; 29327 d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : d >= 1 ? 1 : getDecimalRoundNum(d); 29328 return pow10 * d; 29329} 29330//#endregion 29331//#region src/ui/control/fullscreen_control.ts 29332/** 29333* The event class for fullscreen control events (`fullscreenstart` and `fullscreenend`). 29334* 29335* @group Event Related 29336*/ 29337var FullscreenEvent = class extends Event {}; 29338/** 29339* A `FullscreenControl` control contains a button for toggling the map in and out of fullscreen mode. 29340* When [requestFullscreen](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen) is not supported, fullscreen is handled via CSS properties. 29341* The map's `cooperativeGestures` option is temporarily disabled while the map 29342* is in fullscreen mode, and is restored when the map exist fullscreen mode. 29343* 29344* @group Markers and Controls 29345* @param options - the full screen control options 29346* 29347* @example 29348* ```ts 29349* map.addControl(new FullscreenControl({container: document.querySelector('body')})); 29350* ``` 29351* @see [View a fullscreen map](https://maplibre.org/maplibre-gl-js/docs/examples/view-a-fullscreen-map/) 29352* 29353* ## Events 29354* 29355* **Event** `fullscreenstart` of type {@link FullscreenEvent} will be fired when fullscreen mode has started. 29356* 29357* **Event** `fullscreenend` of type {@link FullscreenEvent} will be fired when fullscreen mode has ended. 29358*/ 29359var FullscreenControl = class extends Evented { 29360 /** 29361 * @param options - the control's options 29362 */ 29363 constructor(options = {}) { 29364 super(); 29365 this._onFullscreenChange = () => { 29366 let fullscreenElement = window.document.fullscreenElement || window.document.webkitFullscreenElement; 29367 while (fullscreenElement?.shadowRoot?.fullscreenElement) fullscreenElement = fullscreenElement.shadowRoot.fullscreenElement; 29368 if (fullscreenElement === this._container !== this._fullscreen) this._handleFullscreenChange(); 29369 }; 29370 this._onClickFullscreen = () => { 29371 if (this._isFullscreen()) this._exitFullscreen(); 29372 else this._requestFullscreen(); 29373 }; 29374 this._fullscreen = false; 29375 this._pseudo = options.pseudo ?? false; 29376 if (options?.container) { 29377 if (options.container instanceof HTMLElement) this._container = options.container; 29378 else warnOnce("Full screen control 'container' must be a DOM element."); 29379 } 29380 if ("onfullscreenchange" in document) this._fullscreenchange = "fullscreenchange"; 29381 else if ("onmozfullscreenchange" in document) this._fullscreenchange = "mozfullscreenchange"; 29382 else if ("onwebkitfullscreenchange" in document) this._fullscreenchange = "webkitfullscreenchange"; 29383 else if ("onmsfullscreenchange" in document) this._fullscreenchange = "MSFullscreenChange"; 29384 } 29385 /** {@inheritDoc IControl.onAdd} */ 29386 onAdd(map) { 29387 this._map = map; 29388 this._container ||= this._map.getContainer(); 29389 this._controlContainer = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-group"); 29390 this._setupUI(); 29391 return this._controlContainer; 29392 } 29393 /** {@inheritDoc IControl.onRemove} */ 29394 onRemove() { 29395 this._controlContainer.remove(); 29396 this._map = null; 29397 window.document.removeEventListener(this._fullscreenchange, this._onFullscreenChange); 29398 } 29399 _setupUI() { 29400 const button = this._fullscreenButton = DOM.create("button", "maplibregl-ctrl-fullscreen", this._controlContainer); 29401 DOM.create("span", "maplibregl-ctrl-icon", button).setAttribute("aria-hidden", "true"); 29402 button.type = "button"; 29403 this._updateTitle(); 29404 this._fullscreenButton.addEventListener("click", this._onClickFullscreen); 29405 window.document.addEventListener(this._fullscreenchange, this._onFullscreenChange); 29406 } 29407 _updateTitle() { 29408 const title = this._getTitle(); 29409 this._fullscreenButton.setAttribute("aria-label", title); 29410 this._fullscreenButton.title = title; 29411 } 29412 _getTitle() { 29413 return this._map._getUIString(this._isFullscreen() ? "FullscreenControl.Exit" : "FullscreenControl.Enter"); 29414 } 29415 _isFullscreen() { 29416 return this._fullscreen; 29417 } 29418 _handleFullscreenChange() { 29419 this._fullscreen = !this._fullscreen; 29420 this._fullscreenButton.classList.toggle("maplibregl-ctrl-shrink"); 29421 this._fullscreenButton.classList.toggle("maplibregl-ctrl-fullscreen"); 29422 this._updateTitle(); 29423 if (this._fullscreen) { 29424 this.fire(new FullscreenEvent("fullscreenstart")); 29425 this._prevCooperativeGesturesEnabled = this._map.cooperativeGestures.isEnabled(); 29426 this._map.cooperativeGestures.disable(); 29427 } else { 29428 this.fire(new FullscreenEvent("fullscreenend")); 29429 if (this._prevCooperativeGesturesEnabled) this._map.cooperativeGestures.enable(); 29430 } 29431 } 29432 _exitFullscreen() { 29433 if (this._pseudo) this._togglePseudoFullScreen(); 29434 else if (window.document.exitFullscreen) window.document.exitFullscreen(); 29435 else if (window.document.webkitCancelFullScreen) window.document.webkitCancelFullScreen(); 29436 else this._togglePseudoFullScreen(); 29437 } 29438 _requestFullscreen() { 29439 if (this._pseudo) this._togglePseudoFullScreen(); 29440 else if (this._container.requestFullscreen) this._container.requestFullscreen(); 29441 else if (this._container.webkitRequestFullscreen) this._container.webkitRequestFullscreen(); 29442 else this._togglePseudoFullScreen(); 29443 } 29444 _togglePseudoFullScreen() { 29445 this._container.classList.toggle("maplibregl-pseudo-fullscreen"); 29446 this._handleFullscreenChange(); 29447 this._map.resize(); 29448 } 29449}; 29450//#endregion 29451//#region src/ui/control/terrain_control.ts 29452/** 29453* A `TerrainControl` control contains a button for turning the terrain on and off. 29454* 29455* @group Markers and Controls 29456* 29457* @example 29458* ```ts 29459* let map = new Map({TerrainControl: false}) 29460* .addControl(new TerrainControl({ 29461* source: "terrain" 29462* })); 29463* ``` 29464* @see [3D Terrain](https://maplibre.org/maplibre-gl-js/docs/examples/3d-terrain/) 29465* @see [Create a Heatmap layer on a globe with terrain elevation](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-heatmap-layer-on-a-globe-with-terrain-elevation/) 29466* @see [Display a hybrid satellite map with terrain elevation](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-hybrid-satellite-map-with-terrain-elevation/) 29467* @see [Sky, Fog, Terrain](https://maplibre.org/maplibre-gl-js/docs/examples/sky-fog-terrain/) 29468*/ 29469var TerrainControl = class { 29470 /** 29471 * @param options - the control's options 29472 */ 29473 constructor(options) { 29474 this._toggleTerrain = () => { 29475 if (this._map.getTerrain()) this._map.setTerrain(null); 29476 else this._map.setTerrain(this.options); 29477 this._updateTerrainIcon(); 29478 }; 29479 this._updateTerrainIcon = () => { 29480 this._terrainButton.classList.remove("maplibregl-ctrl-terrain"); 29481 this._terrainButton.classList.remove("maplibregl-ctrl-terrain-enabled"); 29482 if (this._map.terrain) { 29483 this._terrainButton.classList.add("maplibregl-ctrl-terrain-enabled"); 29484 this._terrainButton.title = this._map._getUIString("TerrainControl.Disable"); 29485 } else { 29486 this._terrainButton.classList.add("maplibregl-ctrl-terrain"); 29487 this._terrainButton.title = this._map._getUIString("TerrainControl.Enable"); 29488 } 29489 }; 29490 this.options = options; 29491 } 29492 /** {@inheritDoc IControl.onAdd} */ 29493 onAdd(map) { 29494 this._map = map; 29495 this._container = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-group"); 29496 this._terrainButton = DOM.create("button", "maplibregl-ctrl-terrain", this._container); 29497 DOM.create("span", "maplibregl-ctrl-icon", this._terrainButton).setAttribute("aria-hidden", "true"); 29498 this._terrainButton.type = "button"; 29499 this._terrainButton.addEventListener("click", this._toggleTerrain); 29500 this._updateTerrainIcon(); 29501 this._map.on("terrain", this._updateTerrainIcon); 29502 return this._container; 29503 } 29504 /** {@inheritDoc IControl.onRemove} */ 29505 onRemove() { 29506 this._container.remove(); 29507 this._map.off("terrain", this._updateTerrainIcon); 29508 this._map = void 0; 29509 } 29510}; 29511//#endregion 29512//#region src/ui/control/globe_control.ts 29513/** 29514* A `GlobeControl` control contains a button for toggling the map projection between "mercator" and "globe". 29515* 29516* @group Markers and Controls 29517* 29518* @example 29519* ```ts 29520* let map = new Map() 29521* .addControl(new GlobeControl()); 29522* ``` 29523* 29524* @see [Display a globe with a fill extrusion layer](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-globe-with-a-fill-extrusion-layer/) 29525* @see [Sky, Fog, Terrain](https://maplibre.org/maplibre-gl-js/docs/examples/sky-fog-terrain/) 29526*/ 29527var GlobeControl = class { 29528 constructor() { 29529 this._toggleProjection = () => { 29530 const currentProjection = this._map.getProjection()?.type; 29531 if (currentProjection === "mercator" || !currentProjection) this._map.setProjection({ type: "globe" }); 29532 else this._map.setProjection({ type: "mercator" }); 29533 this._updateGlobeIcon(); 29534 }; 29535 this._updateGlobeIcon = () => { 29536 this._globeButton.classList.remove("maplibregl-ctrl-globe"); 29537 this._globeButton.classList.remove("maplibregl-ctrl-globe-enabled"); 29538 if (this._map.getProjection()?.type === "globe") { 29539 this._globeButton.classList.add("maplibregl-ctrl-globe-enabled"); 29540 this._globeButton.title = this._map._getUIString("GlobeControl.Disable"); 29541 } else { 29542 this._globeButton.classList.add("maplibregl-ctrl-globe"); 29543 this._globeButton.title = this._map._getUIString("GlobeControl.Enable"); 29544 } 29545 }; 29546 } 29547 /** {@inheritDoc IControl.onAdd} */ 29548 onAdd(map) { 29549 this._map = map; 29550 this._container = DOM.create("div", "maplibregl-ctrl maplibregl-ctrl-group"); 29551 this._globeButton = DOM.create("button", "maplibregl-ctrl-globe", this._container); 29552 DOM.create("span", "maplibregl-ctrl-icon", this._globeButton).setAttribute("aria-hidden", "true"); 29553 this._globeButton.type = "button"; 29554 this._globeButton.addEventListener("click", this._toggleProjection); 29555 this._updateGlobeIcon(); 29556 this._map.on("styledata", this._updateGlobeIcon); 29557 this._map.on("projectiontransition", this._updateGlobeIcon); 29558 return this._container; 29559 } 29560 /** {@inheritDoc IControl.onRemove} */ 29561 onRemove() { 29562 this._container.remove(); 29563 this._map.off("styledata", this._updateGlobeIcon); 29564 this._map.off("projectiontransition", this._updateGlobeIcon); 29565 this._globeButton.removeEventListener("click", this._toggleProjection); 29566 this._map = void 0; 29567 } 29568}; 29569//#endregion 29570//#region src/ui/popup.ts 29571const defaultOptions = { 29572 closeButton: true, 29573 closeOnClick: true, 29574 focusAfterOpen: true, 29575 className: "", 29576 maxWidth: "240px", 29577 subpixelPositioning: false, 29578 locationOccludedOpacity: void 0, 29579 padding: void 0 29580}; 29581const focusQuerySelector = [ 29582 "a[href]", 29583 "[tabindex]:not([tabindex='-1'])", 29584 "[contenteditable]:not([contenteditable='false'])", 29585 "button:not([disabled])", 29586 "input:not([disabled])", 29587 "select:not([disabled])", 29588 "textarea:not([disabled])" 29589].join(", "); 29590/** 29591* The event class for popup events (`open` and `close`). 29592* 29593* @group Event Related 29594*/ 29595var PopupEvent = class extends Event {}; 29596/** 29597* A popup component. 29598* 29599* @group Markers and Controls 29600* 29601* 29602* @example 29603* Create a popup 29604* ```ts 29605* let popup = new Popup(); 29606* // Set an event listener that will fire 29607* // any time the popup is opened 29608* popup.on('open', () => { 29609* console.log('popup was opened'); 29610* }); 29611* ``` 29612* 29613* @example 29614* Create a popup 29615* ```ts 29616* let popup = new Popup(); 29617* // Set an event listener that will fire 29618* // any time the popup is closed 29619* popup.on('close', () => { 29620* console.log('popup was closed'); 29621* }); 29622* ``` 29623* 29624* @example 29625* ```ts 29626* let markerHeight = 50, markerRadius = 10, linearOffset = 25; 29627* let popupOffsets = { 29628* 'top': [0, 0], 29629* 'top-left': [0,0], 29630* 'top-right': [0,0], 29631* 'bottom': [0, -markerHeight], 29632* 'bottom-left': [linearOffset, (markerHeight - markerRadius + linearOffset) * -1], 29633* 'bottom-right': [-linearOffset, (markerHeight - markerRadius + linearOffset) * -1], 29634* 'left': [markerRadius, (markerHeight - markerRadius) * -1], 29635* 'right': [-markerRadius, (markerHeight - markerRadius) * -1] 29636* }; 29637* let popup = new Popup({offset: popupOffsets, className: 'my-class'}) 29638* .setLngLat(e.lngLat) 29639* .setHTML("<h1>Hello World!</h1>") 29640* .setMaxWidth("300px") 29641* .addTo(map); 29642* ``` 29643* @see [Display a popup](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup/) 29644* @see [Display a popup on hover](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-hover/) 29645* @see [Display a popup on click](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-click/) 29646* @see [Attach a popup to a marker instance](https://maplibre.org/maplibre-gl-js/docs/examples/attach-a-popup-to-a-marker-instance/) 29647* @see [Show polygon information on click](https://maplibre.org/maplibre-gl-js/docs/examples/show-polygon-information-on-click/) 29648* 29649* ## Events 29650* 29651* **Event** `open` of type {@link PopupEvent} will be fired when the popup is opened manually or programmatically. 29652* 29653* **Event** `close` of type {@link PopupEvent} will be fired when the popup is closed manually or programmatically. 29654*/ 29655var Popup = class extends Evented { 29656 /** 29657 * @param options - the options 29658 */ 29659 constructor(options) { 29660 super(); 29661 this._updateOpacity = () => { 29662 if (this.options.locationOccludedOpacity === void 0) return; 29663 if (this._map._camera.transform.isLocationOccluded(this.getLngLat())) this._container.style.opacity = `${this.options.locationOccludedOpacity}`; 29664 else this._container.style.opacity = ""; 29665 }; 29666 this.remove = () => { 29667 if (this._content) this._content.remove(); 29668 if (this._container) { 29669 this._container.remove(); 29670 delete this._container; 29671 } 29672 if (this._map) { 29673 this._map.off("move", this._update); 29674 this._map.off("move", this._onClose); 29675 this._map.off("click", this._onClose); 29676 this._map.off("remove", this.remove); 29677 this._map.off("terrain", this._update); 29678 this._map.off("projectiontransition", this._update); 29679 this._map.off("idle", this._update); 29680 this._map.off("mousemove", this._update); 29681 this._map.off("mouseup", this._update); 29682 this._map.off("drag", this._update); 29683 this._map._canvasContainer.classList.remove("maplibregl-track-pointer"); 29684 delete this._map; 29685 this.fire(new PopupEvent("close")); 29686 } 29687 return this; 29688 }; 29689 this._update = (event) => { 29690 const hasPosition = this._lngLat || this._trackPointer; 29691 if (!this._map || !hasPosition || !this._content) return; 29692 if (!this._container) { 29693 this._container = DOM.create("div", "maplibregl-popup", this._map.getContainer()); 29694 this._tip = DOM.create("div", "maplibregl-popup-tip", this._container); 29695 this._container.appendChild(this._content); 29696 if (this.options.className) for (const name of this.options.className.split(" ")) this._container.classList.add(name); 29697 if (this._closeButton) this._closeButton.setAttribute("aria-label", this._map._getUIString("Popup.Close")); 29698 if (this._trackPointer) this._container.classList.add("maplibregl-popup-track-pointer"); 29699 } 29700 if (this.options.maxWidth && this._container.style.maxWidth !== this.options.maxWidth) this._container.style.maxWidth = this.options.maxWidth; 29701 this._lngLat = smartWrap(this._lngLat, this._flatPos, this._map._camera.transform, this._trackPointer); 29702 let cursor; 29703 if (event && "point" in event && event.point) cursor = event.point; 29704 if (this._trackPointer && !cursor) return; 29705 const pos = this._flatPos = this._pos = this._trackPointer && cursor ? cursor : this._map.project(this._lngLat); 29706 if (this._map.terrain) this._flatPos = this._trackPointer && cursor ? cursor : this._map._camera.transform.locationToScreenPoint(this._lngLat); 29707 let anchor = this.options.anchor; 29708 const offset = normalizeOffset(this.options.offset); 29709 if (!anchor) { 29710 const width = this._container.offsetWidth; 29711 const height = this._container.offsetHeight; 29712 const padding = normalizePadding(this.options.padding); 29713 let anchorComponents; 29714 if (pos.y + offset.bottom.y < height + padding.top) anchorComponents = ["top"]; 29715 else if (pos.y > this._map._camera.transform.height - height - padding.bottom) anchorComponents = ["bottom"]; 29716 else anchorComponents = []; 29717 if (pos.x < width / 2 + padding.left) anchorComponents.push("left"); 29718 else if (pos.x > this._map._camera.transform.width - width / 2 - padding.right) anchorComponents.push("right"); 29719 if (anchorComponents.length === 0) anchor = "bottom"; 29720 else anchor = anchorComponents.join("-"); 29721 } 29722 let offsetedPos = pos.add(offset[anchor]); 29723 if (!this.options.subpixelPositioning) offsetedPos = offsetedPos.round(); 29724 this._container.style.transform = `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`; 29725 applyAnchorClass(this._container, anchor, "popup"); 29726 this._updateOpacity(); 29727 }; 29728 this._onClose = () => { 29729 this.remove(); 29730 }; 29731 this.options = extend(Object.create(defaultOptions), options); 29732 } 29733 /** 29734 * Adds the popup to a map. 29735 * 29736 * @param map - The MapLibre GL JS map to add the popup to. 29737 * @example 29738 * ```ts 29739 * new Popup() 29740 * .setLngLat([0, 0]) 29741 * .setHTML("<h1>Null Island</h1>") 29742 * .addTo(map); 29743 * ``` 29744 * @see [Display a popup](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup/) 29745 * @see [Display a popup on hover](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-hover/) 29746 * @see [Display a popup on click](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-click/) 29747 * @see [Show polygon information on click](https://maplibre.org/maplibre-gl-js/docs/examples/show-polygon-information-on-click/) 29748 */ 29749 addTo(map) { 29750 if (this._map) this.remove(); 29751 this._map = map; 29752 if (this.options.closeOnClick) this._map.on("click", this._onClose); 29753 if (this.options.closeOnMove) this._map.on("move", this._onClose); 29754 this._map.on("remove", this.remove); 29755 this._map.on("terrain", this._update); 29756 this._map.on("projectiontransition", this._update); 29757 this._map.on("idle", this._update); 29758 this._update(); 29759 this._focusFirstElement(); 29760 if (this._trackPointer) { 29761 this._map.on("mousemove", this._update); 29762 this._map.on("mouseup", this._update); 29763 if (this._container) this._container.classList.add("maplibregl-popup-track-pointer"); 29764 this._map._canvasContainer.classList.add("maplibregl-track-pointer"); 29765 } else this._map.on("move", this._update); 29766 this.fire(new PopupEvent("open")); 29767 return this; 29768 } 29769 /** 29770 * @returns `true` if the popup is open, `false` if it is closed. 29771 */ 29772 isOpen() { 29773 return !!this._map; 29774 } 29775 /** 29776 * Returns the geographical location of the popup's anchor. 29777 * 29778 * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously 29779 * set by `setLngLat` because `Popup` wraps the anchor longitude across copies of the world to keep 29780 * the popup on screen. 29781 * 29782 * @returns The geographical location of the popup's anchor. 29783 */ 29784 getLngLat() { 29785 return this._lngLat; 29786 } 29787 /** 29788 * Sets the geographical location of the popup's anchor, and moves the popup to it. Replaces trackPointer() behavior. 29789 * 29790 * @param lnglat - The geographical location to set as the popup's anchor. 29791 */ 29792 setLngLat(lnglat) { 29793 this._lngLat = LngLat.convert(lnglat); 29794 this._pos = null; 29795 this._flatPos = null; 29796 this._trackPointer = false; 29797 this._update(); 29798 if (this._map) { 29799 this._map.on("move", this._update); 29800 this._map.off("mousemove", this._update); 29801 if (this._container) this._container.classList.remove("maplibregl-popup-track-pointer"); 29802 this._map._canvasContainer.classList.remove("maplibregl-track-pointer"); 29803 } 29804 return this; 29805 } 29806 /** 29807 * Tracks the popup anchor to the cursor position on screens with a pointer device (it will be hidden on touchscreens). Replaces the `setLngLat` behavior. 29808 * For most use cases, set `closeOnClick` and `closeButton` to `false`. 29809 * @example 29810 * ```ts 29811 * let popup = new Popup({ closeOnClick: false, closeButton: false }) 29812 * .setHTML("<h1>Hello World!</h1>") 29813 * .trackPointer() 29814 * .addTo(map); 29815 * ``` 29816 */ 29817 trackPointer() { 29818 this._trackPointer = true; 29819 this._pos = null; 29820 this._flatPos = null; 29821 this._update(); 29822 if (this._map) { 29823 this._map.off("move", this._update); 29824 this._map.on("mousemove", this._update); 29825 this._map.on("drag", this._update); 29826 if (this._container) this._container.classList.add("maplibregl-popup-track-pointer"); 29827 this._map._canvasContainer.classList.add("maplibregl-track-pointer"); 29828 } 29829 return this; 29830 } 29831 /** 29832 * Returns the `Popup`'s HTML element. 29833 * @example 29834 * Change the `Popup` element's font size 29835 * ```ts 29836 * let popup = new Popup() 29837 * .setLngLat([-96, 37.8]) 29838 * .setHTML("<p>Hello World!</p>") 29839 * .addTo(map); 29840 * let popupElem = popup.getElement(); 29841 * popupElem.style.fontSize = "25px"; 29842 * ``` 29843 * @returns element 29844 */ 29845 getElement() { 29846 return this._container; 29847 } 29848 /** 29849 * Sets the popup's content to a string of text. 29850 * 29851 * This function creates a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node in the DOM, 29852 * so it cannot insert raw HTML. Use this method for security against XSS 29853 * if the popup content is user-provided. 29854 * 29855 * @param text - Textual content for the popup. 29856 * @example 29857 * ```ts 29858 * let popup = new Popup() 29859 * .setLngLat(e.lngLat) 29860 * .setText('Hello, world!') 29861 * .addTo(map); 29862 * ``` 29863 */ 29864 setText(text) { 29865 return this.setDOMContent(document.createTextNode(text)); 29866 } 29867 /** 29868 * Sets the popup's content to the HTML provided as a string. 29869 * 29870 * This method does not perform HTML filtering or sanitization, and must be 29871 * used only with trusted content. Consider {@link Popup.setText} if 29872 * the content is an untrusted text string. 29873 * 29874 * @param html - A string representing HTML content for the popup. 29875 * @example 29876 * ```ts 29877 * let popup = new Popup() 29878 * .setLngLat(e.lngLat) 29879 * .setHTML("<h1>Hello World!</h1>") 29880 * .addTo(map); 29881 * ``` 29882 * @see [Display a popup](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup/) 29883 * @see [Display a popup on hover](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-hover/) 29884 * @see [Display a popup on click](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-click/) 29885 * @see [Attach a popup to a marker instance](https://maplibre.org/maplibre-gl-js/docs/examples/attach-a-popup-to-a-marker-instance/) 29886 */ 29887 setHTML(html) { 29888 const frag = document.createDocumentFragment(); 29889 const temp = document.createElement("body"); 29890 let child; 29891 temp.innerHTML = html; 29892 while (true) { 29893 child = temp.firstChild; 29894 if (!child) break; 29895 frag.appendChild(child); 29896 } 29897 return this.setDOMContent(frag); 29898 } 29899 /** 29900 * Returns the popup's maximum width. 29901 * 29902 * @returns The maximum width of the popup. 29903 */ 29904 getMaxWidth() { 29905 return this._container?.style.maxWidth; 29906 } 29907 /** 29908 * Sets the popup's maximum width. This is setting the CSS property `max-width`. 29909 * Available values can be found here: https://developer.mozilla.org/en-US/docs/Web/CSS/max-width 29910 * 29911 * @param maxWidth - A string representing the value for the maximum width. 29912 */ 29913 setMaxWidth(maxWidth) { 29914 this.options.maxWidth = maxWidth; 29915 this._update(); 29916 return this; 29917 } 29918 /** 29919 * Sets the popup's content to the element provided as a DOM node. 29920 * 29921 * @param htmlNode - A DOM node to be used as content for the popup. 29922 * @example 29923 * Create an element with the popup content 29924 * ```ts 29925 * let div = document.createElement('div'); 29926 * div.innerHTML = 'Hello, world!'; 29927 * let popup = new Popup() 29928 * .setLngLat(e.lngLat) 29929 * .setDOMContent(div) 29930 * .addTo(map); 29931 * ``` 29932 */ 29933 setDOMContent(htmlNode) { 29934 if (this._content) { 29935 while (this._content.hasChildNodes()) if (this._content.firstChild) this._content.removeChild(this._content.firstChild); 29936 } else this._content = DOM.create("div", "maplibregl-popup-content", this._container); 29937 this._content.appendChild(htmlNode); 29938 this._createCloseButton(); 29939 this._update(); 29940 this._focusFirstElement(); 29941 return this; 29942 } 29943 /** 29944 * Adds a CSS class to the popup container element. 29945 * 29946 * @param className - Non-empty string with CSS class name to add to popup container 29947 * 29948 * @example 29949 * ```ts 29950 * let popup = new Popup() 29951 * popup.addClassName('some-class') 29952 * ``` 29953 */ 29954 addClassName(className) { 29955 if (this._container) this._container.classList.add(className); 29956 return this; 29957 } 29958 /** 29959 * Removes a CSS class from the popup container element. 29960 * 29961 * @param className - Non-empty string with CSS class name to remove from popup container 29962 * 29963 * @example 29964 * ```ts 29965 * let popup = new Popup() 29966 * popup.removeClassName('some-class') 29967 * ``` 29968 */ 29969 removeClassName(className) { 29970 if (this._container) this._container.classList.remove(className); 29971 return this; 29972 } 29973 /** 29974 * Sets the popup's offset. 29975 * 29976 * @param offset - Sets the popup's offset. 29977 */ 29978 setOffset(offset) { 29979 this.options.offset = offset; 29980 this._update(); 29981 return this; 29982 } 29983 /** 29984 * Add or remove the given CSS class on the popup container, depending on whether the container currently has that class. 29985 * 29986 * @param className - Non-empty string with CSS class name to add/remove 29987 * 29988 * @returns if the class was removed return false, if class was added, then return true, undefined if there is no container 29989 * 29990 * @example 29991 * ```ts 29992 * let popup = new Popup() 29993 * popup.toggleClassName('toggleClass') 29994 * ``` 29995 */ 29996 toggleClassName(className) { 29997 if (this._container) return this._container.classList.toggle(className); 29998 } 29999 /** 30000 * Set the option to allow subpixel positioning of the popup by passing a boolean 30001 * 30002 * @param value - When boolean is true, subpixel positioning is enabled for the popup. 30003 * 30004 * @example 30005 * ```ts 30006 * let popup = new Popup() 30007 * popup.setSubpixelPositioning(true); 30008 * ``` 30009 */ 30010 setSubpixelPositioning(value) { 30011 this.options.subpixelPositioning = value; 30012 } 30013 /** 30014 * Sets the popup's padding constraints for positioning. 30015 * 30016 * @param padding - The padding to apply as a {@link PaddingOptions} object. 30017 * @example 30018 * ```ts 30019 * popup.setPadding({ top: 10, right: 20, bottom: 30, left: 40 }); 30020 * ``` 30021 */ 30022 setPadding(padding) { 30023 this.options.padding = padding; 30024 this._update(); 30025 } 30026 _createCloseButton() { 30027 if (this.options.closeButton) { 30028 this._closeButton = DOM.create("button", "maplibregl-popup-close-button", this._content); 30029 this._closeButton.type = "button"; 30030 this._closeButton.innerHTML = "×"; 30031 this._closeButton.addEventListener("click", this._onClose); 30032 } 30033 } 30034 _focusFirstElement() { 30035 if (!this.options.focusAfterOpen || !this._container) return; 30036 const firstFocusable = this._container.querySelector(focusQuerySelector); 30037 if (firstFocusable) firstFocusable.focus(); 30038 } 30039}; 30040function normalizeOffset(offset) { 30041 if (!offset) return normalizeOffset(new Point(0, 0)); 30042 else if (typeof offset === "number") { 30043 const cornerOffset = Math.round(Math.abs(offset) / Math.SQRT2); 30044 return { 30045 "center": new Point(0, 0), 30046 "top": new Point(0, offset), 30047 "top-left": new Point(cornerOffset, cornerOffset), 30048 "top-right": new Point(-cornerOffset, cornerOffset), 30049 "bottom": new Point(0, -offset), 30050 "bottom-left": new Point(cornerOffset, -cornerOffset), 30051 "bottom-right": new Point(-cornerOffset, -cornerOffset), 30052 "left": new Point(offset, 0), 30053 "right": new Point(-offset, 0) 30054 }; 30055 } else if (offset instanceof Point || Array.isArray(offset)) { 30056 const convertedOffset = Point.convert(offset); 30057 return { 30058 "center": convertedOffset, 30059 "top": convertedOffset, 30060 "top-left": convertedOffset, 30061 "top-right": convertedOffset, 30062 "bottom": convertedOffset, 30063 "bottom-left": convertedOffset, 30064 "bottom-right": convertedOffset, 30065 "left": convertedOffset, 30066 "right": convertedOffset 30067 }; 30068 } else return { 30069 "center": Point.convert(offset["center"] || [0, 0]), 30070 "top": Point.convert(offset["top"] || [0, 0]), 30071 "top-left": Point.convert(offset["top-left"] || [0, 0]), 30072 "top-right": Point.convert(offset["top-right"] || [0, 0]), 30073 "bottom": Point.convert(offset["bottom"] || [0, 0]), 30074 "bottom-left": Point.convert(offset["bottom-left"] || [0, 0]), 30075 "bottom-right": Point.convert(offset["bottom-right"] || [0, 0]), 30076 "left": Point.convert(offset["left"] || [0, 0]), 30077 "right": Point.convert(offset["right"] || [0, 0]) 30078 }; 30079} 30080function normalizePadding(padding) { 30081 if (!padding) return { 30082 top: 0, 30083 right: 0, 30084 bottom: 0, 30085 left: 0 30086 }; 30087 return { 30088 top: padding.top ?? 0, 30089 right: padding.right ?? 0, 30090 bottom: padding.bottom ?? 0, 30091 left: padding.left ?? 0 30092 }; 30093} 30094//#endregion 30095//#region src/index.ts 30096const version = version$2; 30097/** 30098* Sets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text). 30099* 30100* @deprecated MapLibre shapes Arabic and reorders bidirectional text itself, so nothing has to be 30101* loaded for right-to-left languages to be drawn correctly. A plugin set here still replaces the 30102* built-in implementation, but this will be removed in a future release. 30103* @param pluginURL - URL pointing to the Mapbox RTL text plugin source. 30104* @param lazy - If set to `true`, maplibre will defer loading the plugin until rtl text is encountered, 30105* rtl text will then be rendered only after the plugin finishes loading. 30106* @example 30107* ```ts 30108* setRTLTextPlugin('https://unpkg.com/@mapbox/[email protected]/dist/mapbox-gl-rtl-text.js', false); 30109* ``` 30110* @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/add-support-for-right-to-left-scripts/) 30111* @see [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/) 30112*/ 30113function setRTLTextPlugin(pluginURL, lazy) { 30114 return rtlMainThreadPluginFactory().setRTLTextPlugin(pluginURL, lazy); 30115} 30116/** 30117* Gets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text) status. 30118* The status can be `unavailable` (i.e. not requested or removed), `loading`, `loaded` or `error`. 30119* If the status is `loaded` and the plugin is requested again, an error will be thrown. 30120* 30121* @deprecated The status says nothing about whether right-to-left text can be drawn, which it always 30122* can be. It reports only on a plugin set through the deprecated {@link setRTLTextPlugin}. 30123* @example 30124* ```ts 30125* const pluginStatus = getRTLTextPluginStatus(); 30126* ``` 30127*/ 30128function getRTLTextPluginStatus() { 30129 return rtlMainThreadPluginFactory().getRTLTextPluginStatus(); 30130} 30131/** 30132* Returns the package version of the library 30133* @returns Package version of the library 30134*/ 30135function getVersion() { 30136 return version; 30137} 30138/** 30139* Gets the number of web workers instantiated on a page with GL JS maps. 30140* By default, workerCount is 1 except for Safari browser where it is set to half the number of CPU cores (capped at 3). 30141* Make sure to set this property before creating any map instances for it to have effect. 30142* 30143* @returns Number of workers currently configured. 30144* @example 30145* ```ts 30146* const workerCount = getWorkerCount() 30147* ``` 30148*/ 30149function getWorkerCount() { 30150 return WorkerPool.workerCount; 30151} 30152/** 30153* Sets the number of web workers instantiated on a page with GL JS maps. 30154* By default, workerCount is 1 except for Safari browser where it is set to half the number of CPU cores (capped at 3). 30155* Make sure to set this property before creating any map instances for it to have effect. 30156* 30157* @example 30158* ```ts 30159* setWorkerCount(2); 30160* ``` 30161*/ 30162function setWorkerCount(count) { 30163 WorkerPool.workerCount = count; 30164} 30165/** 30166* Gets and sets the maximum number of images (raster tiles, sprites, icons) to load in parallel, 30167* which affects performance in raster-heavy maps. 16 by default. 30168* 30169* @returns Number of parallel requests currently configured. 30170* @example 30171* ```ts 30172* getMaxParallelImageRequests(); 30173* ``` 30174*/ 30175function getMaxParallelImageRequests() { 30176 return config.MAX_PARALLEL_IMAGE_REQUESTS; 30177} 30178/** 30179* Sets the maximum number of images (raster tiles, sprites, icons) to load in parallel, 30180* which affects performance in raster-heavy maps. 16 by default. 30181* 30182* @example 30183* ```ts 30184* setMaxParallelImageRequests(10); 30185* ``` 30186*/ 30187function setMaxParallelImageRequests(numRequests) { 30188 config.MAX_PARALLEL_IMAGE_REQUESTS = numRequests; 30189} 30190/** 30191* Gets the worker url 30192* @returns The worker url 30193*/ 30194function getWorkerUrl() { 30195 return config.WORKER_URL; 30196} 30197/** 30198* Sets the worker url 30199*/ 30200function setWorkerUrl(value) { 30201 config.WORKER_URL = value; 30202} 30203/** 30204* Allows loading javascript code in the worker thread. 30205* *Note* that since this is using some very internal classes and flows it is considered experimental and can break at any point. 30206* 30207* It can be useful for the following examples: 30208* 1. Using `self.addProtocol` in the worker thread - note that you might need to also register the protocol on the main thread. 30209* 2. Using `self.registerWorkerSource(workerSource: WorkerSource)` to register a worker source, which should come with `addSourceType` usually. 30210* 3. using `self.actor.registerMessageHandler` to override some internal worker operations 30211* @param workerUrl - the worker url e.g. a url of a javascript file to load in the worker 30212* @returns 30213* 30214* @example 30215* ```ts 30216* // below is an example of sending a js file to the worker to load the method there 30217* // Note that you'll need to call the global function `addProtocol` in the worker to register the protocol there. 30218* // add-protocol-worker.js 30219* async function loadFn(params, abortController) { 30220* const t = await fetch(`https://${params.url.split("://")[1]}`); 30221* if (t.status == 200) { 30222* const buffer = await t.arrayBuffer(); 30223* return {data: buffer} 30224* } else { 30225* throw new Error(`Tile fetch error: ${t.statusText}`); 30226* } 30227* } 30228* self.addProtocol('custom', loadFn); 30229* 30230* // main.js 30231* importScriptInWorkers('add-protocol-worker.js'); 30232* ``` 30233*/ 30234async function importScriptInWorkers(workerUrl) { 30235 await getGlobalDispatcher().broadcast("IS", workerUrl); 30236} 30237//#endregion 30238export { AJAXError, AttributionControl, BoxZoomHandler, CanvasSource, CooperativeGesturesHandler, DoubleClickZoomHandler, DragPanHandler, DragRotateHandler, EXTENT, EdgeInsets, ErrorEvent, Event, Evented, FullscreenControl, FullscreenEvent, GPUInitializationError, GeoJSONSource, GeolocateControl, GeolocateErrorEvent, GeolocateEvent, GeolocatePositionEvent, GlobeControl, Hash, ImageSource, KeyboardHandler, LngLat, LngLatBounds, LogoControl, Map$1 as Map, Map$1 as MapLibreMap, MapBoxZoomEvent, MapContextEvent, MapLibreEvent, MapMouseEvent, MapMovementEvent, MapProjectionEvent, MapSourceDataEvent, MapStyleDataEvent, MapStyleImageMissingEvent, MapStyleLoadEvent, MapTerrainEvent, MapTouchEvent, MapWheelEvent, Marker, MarkerClickEvent, MarkerDragEvent, MercatorCoordinate, NavigationControl, Point, Popup, PopupEvent, RasterDEMTileSource, RasterTileSource, ScaleControl, ScrollZoomHandler, Style, TerrainControl, TwoFingersTouchPitchHandler, TwoFingersTouchRotateHandler, TwoFingersTouchZoomHandler, TwoFingersTouchZoomRotateHandler, VectorTileSource, VideoSource, addProtocol, addSourceType, clearPrewarmedResources, config, createTileMesh, getGlobalDispatcher, getMaxParallelImageRequests, getRTLTextPluginStatus, getVersion, getWorkerCount, getWorkerUrl, importScriptInWorkers, isTimeFrozen, now, prewarm, removeProtocol, restoreNow, setMaxParallelImageRequests, setNow, setRTLTextPlugin, setWorkerCount, setWorkerUrl }; 30239 30240//# sourceMappingURL=maplibre-gl-dev.mjs.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.